termdock 1.4.98 → 1.4.99

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 (69) hide show
  1. package/.desktop-dist/main.js +211 -34
  2. package/dist/client/assets/{ModelPreview-Drqv0OTg.js → ModelPreview-CJK3Nmg4.js} +1 -1
  3. package/dist/client/assets/{abnfDiagram-VRR7QNED-Cl8CT45X.js → abnfDiagram-VRR7QNED-C-QKkcSr.js} +1 -1
  4. package/dist/client/assets/{arc-2Sc638oe.js → arc-BpjFutHZ.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-CNH9w9eN.js → architectureDiagram-ZJ3FMSHR-DeeuzGsk.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-677ZJIJ3-ClvE_yP1.js → blockDiagram-677ZJIJ3-BkDiKR3-.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-LMCZKHZV-CxIMU8l9.js → c4Diagram-LMCZKHZV-BX6424vk.js} +1 -1
  8. package/dist/client/assets/channel-BiaGwVDJ.js +1 -0
  9. package/dist/client/assets/{chunk-2Q5K7J3B-CdvXmc6Z.js → chunk-2Q5K7J3B-DwNMfRvw.js} +1 -1
  10. package/dist/client/assets/{chunk-32BRIVSS-B1dA4l5A.js → chunk-32BRIVSS-D5g3chKu.js} +1 -1
  11. package/dist/client/assets/{chunk-5VM5RSS4-CVk_pzpf.js → chunk-5VM5RSS4-B2rw8Ldu.js} +1 -1
  12. package/dist/client/assets/{chunk-EX3LRPZG-B46zhCa0.js → chunk-EX3LRPZG-j3CVdb45.js} +1 -1
  13. package/dist/client/assets/{chunk-JWPE2WC7-1wRPE3eO.js → chunk-JWPE2WC7-iHTR9CAX.js} +1 -1
  14. package/dist/client/assets/{chunk-MOJQB5TN-BzPrhNIS.js → chunk-MOJQB5TN-DVD1wXze.js} +1 -1
  15. package/dist/client/assets/{chunk-RYQCIY6F-D0sDGnBf.js → chunk-RYQCIY6F-zZRopnRd.js} +1 -1
  16. package/dist/client/assets/{chunk-V7JOEXUC-BRo0103H.js → chunk-V7JOEXUC-h4EZpc4k.js} +1 -1
  17. package/dist/client/assets/{chunk-VR4S4FIN-CW-wSkDE.js → chunk-VR4S4FIN-C9MZH2Oy.js} +1 -1
  18. package/dist/client/assets/{chunk-XXDRQBXY-BuWiQ0uq.js → chunk-XXDRQBXY-BNPDO5fQ.js} +1 -1
  19. package/dist/client/assets/classDiagram-OUVF2IWQ-DYljVZD_.js +1 -0
  20. package/dist/client/assets/classDiagram-v2-EOCWNBFH-DYljVZD_.js +1 -0
  21. package/dist/client/assets/{cose-bilkent-JH36ORCC-WzKVd03Q.js → cose-bilkent-JH36ORCC-DlqULumX.js} +1 -1
  22. package/dist/client/assets/{cynefin-VYW2F7L2-CfXYAqnq.js → cynefin-VYW2F7L2-D6pWZSwi.js} +1 -1
  23. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-BnJGle_X.js → cynefinDiagram-TSTJHNR4-B6SI7XOe.js} +1 -1
  24. package/dist/client/assets/{dagre-VKFMJZFB-CDDlGCQy.js → dagre-VKFMJZFB-DXnKJ4XD.js} +1 -1
  25. package/dist/client/assets/{diagram-FQU43EPY-t8DNZS7P.js → diagram-FQU43EPY-B5NTAhw6.js} +1 -1
  26. package/dist/client/assets/{diagram-G47NLZAW-CZpQd91k.js → diagram-G47NLZAW-Dihaal0B.js} +1 -1
  27. package/dist/client/assets/{diagram-NH7WQ7WH-OET0EFlt.js → diagram-NH7WQ7WH-Be987DPJ.js} +1 -1
  28. package/dist/client/assets/{diagram-OA4YK3LP-COgju4NX.js → diagram-OA4YK3LP-CfgTeXAJ.js} +1 -1
  29. package/dist/client/assets/{diagram-WEI45ONY-6gNSoIek.js → diagram-WEI45ONY-BgrK8rP2.js} +1 -1
  30. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-CTlIMc8W.js → ebnfDiagram-CCIWWBDH-BPsH1_QO.js} +1 -1
  31. package/dist/client/assets/{erDiagram-Q63AITRT-BDsEvZvq.js → erDiagram-Q63AITRT-F3s7Ytrc.js} +1 -1
  32. package/dist/client/assets/{flowDiagram-23GEKE2U-ggl4zaY9.js → flowDiagram-23GEKE2U-ByL-8svD.js} +1 -1
  33. package/dist/client/assets/{ganttDiagram-NO4QXBWP-D9Ca5oWS.js → ganttDiagram-NO4QXBWP-Dtv-fi06.js} +1 -1
  34. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-THPUH6Bg.js → gitGraphDiagram-IHSO6WYX-CeD48POl.js} +1 -1
  35. package/dist/client/assets/{index-BuMMW0wS.js → index-C82Uc6Vy.js} +4 -4
  36. package/dist/client/assets/{infoDiagram-FWYZ7A6U-CKF-vSTz.js → infoDiagram-FWYZ7A6U-BxvLe33p.js} +1 -1
  37. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-CBFlmgI3.js → ishikawaDiagram-FXEZZL3T-XPW1aoJK.js} +1 -1
  38. package/dist/client/assets/{journeyDiagram-5HDEW3XC-lbXIqX5a.js → journeyDiagram-5HDEW3XC-Bhiae04Z.js} +1 -1
  39. package/dist/client/assets/{kanban-definition-HUTT4EX6-BUuO_e54.js → kanban-definition-HUTT4EX6-D5iOY3Zc.js} +1 -1
  40. package/dist/client/assets/{linear-D_uoNzR8.js → linear-PCgLsFTe.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-D4Yy5OxZ.js → mermaid.core-DWuc-xG5.js} +4 -4
  42. package/dist/client/assets/{mindmap-definition-LN4V7U3C-B0Ml-jaq.js → mindmap-definition-LN4V7U3C-C0EFK91v.js} +1 -1
  43. package/dist/client/assets/{pegDiagram-2B236MQR-CKF9cuqx.js → pegDiagram-2B236MQR-ClTz8aHa.js} +1 -1
  44. package/dist/client/assets/{pieDiagram-ENE6RG2P-DL3XisAD.js → pieDiagram-ENE6RG2P-BuTv1DGN.js} +1 -1
  45. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-DrsHXXqd.js → quadrantDiagram-ABIIQ3AL-FLZC-cqI.js} +1 -1
  46. package/dist/client/assets/{railroadDiagram-RFXS5EU6-DMAz8hLg.js → railroadDiagram-RFXS5EU6-DK68FlFh.js} +1 -1
  47. package/dist/client/assets/{requirementDiagram-TGXJPOKE-CCHretnQ.js → requirementDiagram-TGXJPOKE-B_701AJ1.js} +1 -1
  48. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-Btb7ZNwU.js → sankeyDiagram-HTMAVEWB-Bk87L7JG.js} +1 -1
  49. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-DcZk1PdX.js → sequenceDiagram-DBY2YBRQ-Biy5tPTS.js} +1 -1
  50. package/dist/client/assets/{sizeCapture-X5ZJPWSS-vMYb_VZU.js → sizeCapture-X5ZJPWSS-__4zzUuh.js} +1 -1
  51. package/dist/client/assets/{stateDiagram-2N3HPSRC-CltexqJj.js → stateDiagram-2N3HPSRC-B_400cf9.js} +1 -1
  52. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-CK77br3V.js +1 -0
  53. package/dist/client/assets/{swimlanes-5IMT3BWC-DPnwlpXX.js → swimlanes-5IMT3BWC-CxWZz0n0.js} +2 -2
  54. package/dist/client/assets/swimlanesDiagram-G3AALYLV-DbYe8ax3.js +8 -0
  55. package/dist/client/assets/{timeline-definition-FHXFAJF6-l78vfKln.js → timeline-definition-FHXFAJF6-DSrIUp8H.js} +1 -1
  56. package/dist/client/assets/{vennDiagram-L72KCM5P-BggNEe_f.js → vennDiagram-L72KCM5P-CdwaUmpL.js} +1 -1
  57. package/dist/client/assets/{wardleyDiagram-EHGQE667-v04USz2e.js → wardleyDiagram-EHGQE667-DwxlF0Xk.js} +1 -1
  58. package/dist/client/assets/{xychartDiagram-FW5EYKEG-CInHBOpK.js → xychartDiagram-FW5EYKEG-CQNmEc_n.js} +1 -1
  59. package/dist/client/index.html +1 -1
  60. package/dist/client/sw.js +1 -1
  61. package/dist/server/agent/sessionSearchStore.js +38 -10
  62. package/dist/server/routes/terminal.js +206 -30
  63. package/package.json +3 -2
  64. package/runtime-manifest.json +2 -2
  65. package/dist/client/assets/channel-DPt818Jx.js +0 -1
  66. package/dist/client/assets/classDiagram-OUVF2IWQ-C2hZbTnO.js +0 -1
  67. package/dist/client/assets/classDiagram-v2-EOCWNBFH-C2hZbTnO.js +0 -1
  68. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BslbFNw3.js +0 -1
  69. package/dist/client/assets/swimlanesDiagram-G3AALYLV-B0V1XVQb.js +0 -8
@@ -3,6 +3,7 @@ import { execFile, spawn } from 'node:child_process';
3
3
  import crypto from 'node:crypto';
4
4
  import dgram from 'node:dgram';
5
5
  import fs from 'node:fs';
6
+ import https from 'node:https';
6
7
  import os from 'node:os';
7
8
  import path from 'node:path';
8
9
  import { promisify } from 'node:util';
@@ -11,6 +12,7 @@ import { checkForDesktopUpdates, configureDesktopUpdater, ensureLatestRuntime, g
11
12
  import { resolvePackagedRuntime, rollbackDownloadedRuntime, } from './runtime.js';
12
13
  import { isExternalLinkStagingUrl, isSafeExternalUrl } from './externalLinks.js';
13
14
  import { shouldThrottleDesktopRenderer } from './windowPolicy.js';
15
+ import { canOfferCertificateTrust, downloadCertificateAuthority, isCertificateTrustError, isLocalNetworkHostname, } from './certificateTrust.js';
14
16
  const execFileAsync = promisify(execFile);
15
17
  const currentDir = path.dirname(fileURLToPath(import.meta.url));
16
18
  const projectRoot = path.resolve(currentDir, '..');
@@ -22,6 +24,9 @@ const PROTOCOL_VERSION = 1;
22
24
  const HEALTH_TIMEOUT_MS = 3_500;
23
25
  const START_TIMEOUT_MS = 90_000;
24
26
  const localServiceCertificatePath = path.join(termdockDir, 'certs', 'termdock-local.pem');
27
+ const sessionTrustedCertificateTargets = new Set();
28
+ const sessionTrustedCertificateAuthorities = new Map();
29
+ let managedLocalCertificateFingerprint = null;
25
30
  /** Delivered-but-unseen desktop notifications, mirrored into the Dock badge. */
26
31
  const activeNotifications = new Map();
27
32
  let unreadNotificationCount = 0;
@@ -87,17 +92,11 @@ function openExternalLink(url) {
87
92
  return;
88
93
  void shell.openExternal(url);
89
94
  }
90
- function configureLocalServiceCertificateTrust() {
91
- let localFingerprint = null;
92
- try {
93
- localFingerprint = new crypto.X509Certificate(fs.readFileSync(localServiceCertificatePath)).fingerprint256;
94
- }
95
- catch {
96
- // The certificate is created when the local service first enables HTTPS.
97
- }
98
- if (!localFingerprint)
99
- return;
100
- session.defaultSession.setCertificateVerifyProc((request, callback) => {
95
+ function certificateTrustKey(hostname, fingerprint) {
96
+ return `${hostname.toLowerCase().replace(/^\[|\]$/g, '')}\0${fingerprint}`;
97
+ }
98
+ function installCertificateVerifyProcedure(targetSession = session.defaultSession) {
99
+ targetSession.setCertificateVerifyProc((request, callback) => {
101
100
  let isLocalTarget = false;
102
101
  try {
103
102
  isLocalTarget = isLocalNetworkTarget(new URL(`https://${request.hostname}`));
@@ -112,11 +111,30 @@ function configureLocalServiceCertificateTrust() {
112
111
  catch {
113
112
  // Keep Chromium's default verification if the certificate cannot be parsed.
114
113
  }
115
- callback(isLocalTarget && presentedFingerprint === localFingerprint ? 0 : -3);
114
+ const explicitlyTrustedTarget = presentedFingerprint
115
+ ? sessionTrustedCertificateTargets.has(certificateTrustKey(request.hostname, presentedFingerprint))
116
+ : false;
117
+ const managedLocalCertificate = isLocalTarget
118
+ && presentedFingerprint === managedLocalCertificateFingerprint;
119
+ callback(explicitlyTrustedTarget || managedLocalCertificate ? 0 : -3);
116
120
  });
117
121
  }
122
+ function configureLocalServiceCertificateTrust() {
123
+ try {
124
+ managedLocalCertificateFingerprint = new crypto.X509Certificate(fs.readFileSync(localServiceCertificatePath)).fingerprint256;
125
+ }
126
+ catch {
127
+ // The certificate is created when the local service first enables HTTPS.
128
+ }
129
+ installCertificateVerifyProcedure();
130
+ }
118
131
  function defaultConfig() {
119
- return { version: 1, connections: [], lastConnectionUrl: null };
132
+ return {
133
+ version: 1,
134
+ connections: [],
135
+ lastConnectionUrl: null,
136
+ trustedCertificateAuthorities: [],
137
+ };
120
138
  }
121
139
  function normalizeServiceUrl(raw) {
122
140
  const value = raw.trim();
@@ -134,25 +152,7 @@ function normalizeServiceUrl(raw) {
134
152
  return parsed.toString().replace(/\/$/, '');
135
153
  }
136
154
  function isLocalNetworkTarget(url) {
137
- const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
138
- if (hostname === 'localhost'
139
- || hostname.endsWith('.local')
140
- || !hostname.includes('.')) {
141
- return true;
142
- }
143
- if (/^10\./.test(hostname) || /^192\.168\./.test(hostname) || /^169\.254\./.test(hostname)) {
144
- return true;
145
- }
146
- const ipv4 = hostname.match(/^172\.(\d{1,3})\./);
147
- if (ipv4) {
148
- const secondOctet = Number(ipv4[1]);
149
- if (secondOctet >= 16 && secondOctet <= 31)
150
- return true;
151
- }
152
- return hostname === '::1'
153
- || hostname.startsWith('fe80:')
154
- || hostname.startsWith('fc')
155
- || hostname.startsWith('fd');
155
+ return isLocalNetworkHostname(url.hostname);
156
156
  }
157
157
  function networkErrorDetails(error) {
158
158
  if (!(error instanceof Error))
@@ -163,6 +163,72 @@ function networkErrorDetails(error) {
163
163
  function looksLikeLocalNetworkPermissionError(error) {
164
164
  return /ERR_ADDRESS_UNREACHABLE|ERR_NETWORK_ACCESS_DENIED/i.test(networkErrorDetails(error));
165
165
  }
166
+ function certificateDetail(target, certificate) {
167
+ return [
168
+ `目标:${target}`,
169
+ `CA:${certificate.subject.replace(/\n/g, ', ')}`,
170
+ `CA SHA-256:${certificate.fingerprint256}`,
171
+ `服务证书 SHA-256:${certificate.leafFingerprint256}`,
172
+ `有效期:${certificate.validFrom} — ${certificate.validTo}`,
173
+ '',
174
+ '继续后仅 Termdock 会信任该目标使用的这张 CA,不会修改 macOS 系统钥匙串。请只信任你确认属于该服务的证书。',
175
+ ].join('\n');
176
+ }
177
+ function trustedCertificateAuthorityFor(target) {
178
+ const origin = new URL(target).origin;
179
+ return readDesktopConfig().trustedCertificateAuthorities.find((entry) => entry.origin === origin);
180
+ }
181
+ async function requestCertificateTrust(target) {
182
+ let certificate;
183
+ try {
184
+ certificate = await downloadCertificateAuthority(target);
185
+ }
186
+ catch (error) {
187
+ await showDesktopMessageBox({
188
+ type: 'error',
189
+ title: '无法获取可信的 CA 证书',
190
+ message: `无法为 ${target} 准备证书信任`,
191
+ detail: networkErrorDetails(error),
192
+ });
193
+ return null;
194
+ }
195
+ const existingTrust = trustedCertificateAuthorityFor(target);
196
+ if (existingTrust?.fingerprint256 === certificate.fingerprint256) {
197
+ sessionTrustedCertificateAuthorities.set(new URL(target).origin, certificate.certificatePem);
198
+ sessionTrustedCertificateTargets.add(certificateTrustKey(new URL(target).hostname, certificate.leafFingerprint256));
199
+ return certificate;
200
+ }
201
+ const confirmation = await showDesktopMessageBox({
202
+ type: 'warning',
203
+ title: '信任 HTTPS 服务证书',
204
+ message: existingTrust
205
+ ? '此服务的 CA 证书已发生变化'
206
+ : '此服务使用了 Termdock 尚未信任的 HTTPS 证书',
207
+ detail: `${certificateDetail(target, certificate)}${existingTrust
208
+ ? `\n\n此前信任的 CA SHA-256:${existingTrust.fingerprint256}`
209
+ : ''}`,
210
+ buttons: ['信任并连接', '取消'],
211
+ defaultId: 1,
212
+ cancelId: 1,
213
+ noLink: true,
214
+ });
215
+ if (confirmation.response !== 0)
216
+ return null;
217
+ const config = readDesktopConfig();
218
+ const origin = new URL(target).origin;
219
+ config.trustedCertificateAuthorities = config.trustedCertificateAuthorities
220
+ .filter((entry) => entry.origin !== origin);
221
+ config.trustedCertificateAuthorities.push({
222
+ origin,
223
+ fingerprint256: certificate.fingerprint256,
224
+ subject: certificate.subject,
225
+ trustedAt: Date.now(),
226
+ });
227
+ writeDesktopConfig(config);
228
+ sessionTrustedCertificateAuthorities.set(origin, certificate.certificatePem);
229
+ sessionTrustedCertificateTargets.add(certificateTrustKey(new URL(target).hostname, certificate.leafFingerprint256));
230
+ return certificate;
231
+ }
166
232
  async function requestLocalNetworkPermissionRetry(target) {
167
233
  const result = await showDesktopMessageBox({
168
234
  type: 'warning',
@@ -190,6 +256,13 @@ function readDesktopConfig() {
190
256
  && typeof entry.label === 'string'
191
257
  && typeof entry.url === 'string'),
192
258
  lastConnectionUrl: typeof parsed.lastConnectionUrl === 'string' ? parsed.lastConnectionUrl : null,
259
+ trustedCertificateAuthorities: Array.isArray(parsed.trustedCertificateAuthorities)
260
+ ? parsed.trustedCertificateAuthorities.filter((entry) => Boolean(entry)
261
+ && typeof entry.origin === 'string'
262
+ && typeof entry.fingerprint256 === 'string'
263
+ && typeof entry.subject === 'string'
264
+ && typeof entry.trustedAt === 'number')
265
+ : [],
193
266
  };
194
267
  }
195
268
  catch {
@@ -250,6 +323,78 @@ async function fetchWithTimeout(url) {
250
323
  clearTimeout(timer);
251
324
  }
252
325
  }
326
+ async function fetchJsonWithCertificateAuthority(rawUrl, certificateAuthority) {
327
+ return await new Promise((resolve, reject) => {
328
+ const request = https.get(rawUrl, {
329
+ agent: false,
330
+ ca: certificateAuthority,
331
+ rejectUnauthorized: true,
332
+ headers: { Accept: 'application/json' },
333
+ }, (response) => {
334
+ const chunks = [];
335
+ let size = 0;
336
+ response.on('data', (chunk) => {
337
+ size += chunk.length;
338
+ if (size > 256 * 1024) {
339
+ request.destroy(new Error('服务响应过大'));
340
+ return;
341
+ }
342
+ chunks.push(chunk);
343
+ });
344
+ response.on('end', () => {
345
+ try {
346
+ const body = Buffer.concat(chunks).toString('utf8');
347
+ resolve({
348
+ status: response.statusCode ?? 0,
349
+ json: body ? JSON.parse(body) : null,
350
+ });
351
+ }
352
+ catch {
353
+ reject(new Error('服务返回了无效的 JSON'));
354
+ }
355
+ });
356
+ });
357
+ request.setTimeout(HEALTH_TIMEOUT_MS, () => request.destroy(new Error('连接超时')));
358
+ request.on('error', reject);
359
+ });
360
+ }
361
+ async function probeServiceWithCertificateAuthority(rawUrl, certificateAuthority) {
362
+ const url = normalizeServiceUrl(rawUrl);
363
+ try {
364
+ const healthResponse = await fetchJsonWithCertificateAuthority(`${url}/health`, certificateAuthority);
365
+ if (healthResponse.status < 200 || healthResponse.status >= 300) {
366
+ return { ok: false, url, error: `健康检查返回 HTTP ${healthResponse.status}` };
367
+ }
368
+ const health = healthResponse.json;
369
+ if (health?.status !== 'ok') {
370
+ return { ok: false, url, error: '目标地址不是可识别的 Termdock 服务' };
371
+ }
372
+ try {
373
+ const metadataResponse = await fetchJsonWithCertificateAuthority(`${url}/api/meta`, certificateAuthority);
374
+ if (metadataResponse.status >= 200 && metadataResponse.status < 300) {
375
+ const metadata = metadataResponse.json;
376
+ if (metadata?.product && metadata.product !== 'termdock') {
377
+ return { ok: false, url, error: '目标服务的产品标识不是 Termdock' };
378
+ }
379
+ return {
380
+ ok: true,
381
+ url,
382
+ version: typeof metadata?.version === 'string' ? metadata.version : undefined,
383
+ protocolVersion: typeof metadata?.protocolVersion === 'number'
384
+ ? metadata.protocolVersion
385
+ : undefined,
386
+ };
387
+ }
388
+ }
389
+ catch {
390
+ // Older Termdock releases do not expose /api/meta.
391
+ }
392
+ return { ok: true, url };
393
+ }
394
+ catch (error) {
395
+ return { ok: false, url, error: networkErrorDetails(error) };
396
+ }
397
+ }
253
398
  async function probeService(rawUrl) {
254
399
  let url;
255
400
  try {
@@ -293,12 +438,27 @@ async function probeService(rawUrl) {
293
438
  catch (error) {
294
439
  const message = error instanceof Error && error.name === 'AbortError'
295
440
  ? '连接超时'
296
- : (error instanceof Error ? error.message : String(error));
441
+ : networkErrorDetails(error);
297
442
  return { ok: false, url, error: message };
298
443
  }
299
444
  }
300
445
  async function probeServiceWithLocalNetworkPermission(rawUrl) {
301
- let probe = await probeService(rawUrl);
446
+ let normalizedUrl;
447
+ try {
448
+ normalizedUrl = normalizeServiceUrl(rawUrl);
449
+ }
450
+ catch {
451
+ return await probeService(rawUrl);
452
+ }
453
+ // Do the first macOS HTTPS probe with Node TLS. Sending an untrusted
454
+ // certificate through Chromium first permanently caches that rejection for
455
+ // the process and prevents an immediate retry after the user approves it.
456
+ const approvedAuthority = sessionTrustedCertificateAuthorities.get(new URL(normalizedUrl).origin);
457
+ let probe = approvedAuthority
458
+ ? await probeServiceWithCertificateAuthority(normalizedUrl, approvedAuthority)
459
+ : canOfferCertificateTrust(normalizedUrl)
460
+ ? await probeServiceWithCertificateAuthority(normalizedUrl)
461
+ : await probeService(normalizedUrl);
302
462
  if (probe.ok)
303
463
  return probe;
304
464
  let parsedProbeUrl;
@@ -308,6 +468,13 @@ async function probeServiceWithLocalNetworkPermission(rawUrl) {
308
468
  catch {
309
469
  return probe;
310
470
  }
471
+ if (canOfferCertificateTrust(probe.url) && isCertificateTrustError(probe.error)) {
472
+ const certificate = await requestCertificateTrust(probe.url);
473
+ if (!certificate) {
474
+ return { ...probe, error: 'HTTPS 证书尚未受信任' };
475
+ }
476
+ return await probeServiceWithCertificateAuthority(rawUrl, certificate.certificatePem);
477
+ }
311
478
  if (!isLocalNetworkTarget(parsedProbeUrl) || !looksLikeLocalNetworkPermissionError(probe.error)) {
312
479
  return probe;
313
480
  }
@@ -685,6 +852,15 @@ async function showConnectionCenter() {
685
852
  showAndFocusWindow(mainWindow);
686
853
  }
687
854
  function createDesktopWindow(options) {
855
+ const serviceSession = options
856
+ ? session.fromPartition(`persist:termdock-service-${crypto
857
+ .createHash('sha256')
858
+ .update(options.serviceOrigin)
859
+ .digest('hex')
860
+ .slice(0, 24)}`)
861
+ : null;
862
+ if (serviceSession)
863
+ installCertificateVerifyProcedure(serviceSession);
688
864
  const window = new BrowserWindow({
689
865
  title: options ? `Termdock — ${options.label}` : 'Termdock — 连接中心',
690
866
  show: false,
@@ -699,6 +875,7 @@ function createDesktopWindow(options) {
699
875
  titleBarStyle: 'hiddenInset',
700
876
  trafficLightPosition: { x: 16, y: 16 },
701
877
  webPreferences: {
878
+ ...(serviceSession ? { session: serviceSession } : {}),
702
879
  preload: path.join(currentDir, 'preload.cjs'),
703
880
  nodeIntegration: false,
704
881
  contextIsolation: true,