codex-overleaf-link 2.3.5 → 2.3.6

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 (33) hide show
  1. package/README.md +31 -24
  2. package/extension/bootstrap/manifest.template.json +1 -1
  3. package/extension/src/background.js +4 -0
  4. package/extension/src/backgroundUpdateCoordinator.js +88 -4
  5. package/extension/src/content/contentRuntime.js +24 -7
  6. package/extension/src/content/diagnosticsController.js +13 -3
  7. package/extension/src/content/diagnosticsPanel.js +1 -1
  8. package/extension/src/content/generated/content.bundle.js +848 -245
  9. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  10. package/extension/src/content/legacyGlobalRegistry.js +2 -0
  11. package/extension/src/content/otWarmMirror.js +95 -65
  12. package/extension/src/content/otWarmMirrorController.js +102 -1
  13. package/extension/src/content/panelMaintenance.js +26 -15
  14. package/extension/src/content/providerSettingsCoordinator.js +1 -0
  15. package/extension/src/content/runFailureNotice.js +104 -0
  16. package/extension/src/content/runScrollLayout.js +124 -0
  17. package/extension/src/content/runTimelineView.js +36 -10
  18. package/extension/src/content/settingsPanel.js +28 -11
  19. package/extension/src/content/updateNotice.js +24 -4
  20. package/extension/src/page/overleafRealtimeObserver.js +116 -24
  21. package/extension/src/shared/compatibility.js +1 -1
  22. package/extension/src/shared/i18n.js +77 -37
  23. package/extension/src/shared/managedUpdateProjection.js +1 -1
  24. package/extension/src/shared/otText.js +0 -1
  25. package/extension/src/shared/sessionState.js +17 -21
  26. package/extension/src/shared/sessionTitle.js +120 -0
  27. package/extension/src/shared/updateRuntimeIdentity.js +32 -0
  28. package/extension/styles/panel.css +73 -14
  29. package/native-host/src/mirrorWorkspace.js +20 -1
  30. package/native-host/src/releaseProxy.js +177 -0
  31. package/native-host/src/releaseTransport.js +154 -0
  32. package/native-host/src/updateManager.js +40 -39
  33. package/package.json +1 -1
@@ -0,0 +1,120 @@
1
+ (function initSessionTitle(root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory();
4
+ } else {
5
+ root.CodexOverleafModuleRegistry.define('SessionTitle', [], factory);
6
+ }
7
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function sessionTitleFactory() {
8
+ 'use strict';
9
+
10
+ const INPUT_LIMIT = 12000;
11
+ const STORAGE_LIMIT = 80;
12
+ const segmenter = typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function'
13
+ ? new Intl.Segmenter(undefined, { granularity: 'grapheme' }) : null;
14
+ const FILE_TOKEN = /@file:(?:"([^"]+)"|'([^']+)'|([^\s]+))/g;
15
+
16
+ function derive(runs, task, options = {}) {
17
+ let fallback = '';
18
+ for (const run of Array.isArray(runs) ? runs : []) {
19
+ const candidate = fromPrompt(run?.task, options);
20
+ fallback ||= candidate;
21
+ if (candidate && !isGeneric(candidate)) return candidate;
22
+ }
23
+ const draft = fromPrompt(task, options);
24
+ return draft && !isGeneric(draft) ? draft : fallback || draft;
25
+ }
26
+
27
+ function fromPrompt(value, { sanitize = text => text, maxChars = 24 } = {}) {
28
+ if (typeof value !== 'string' || !value.trim()) return '';
29
+ const source = String(sanitize(value.slice(0, INPUT_LIMIT)) || '');
30
+ let fence = null;
31
+ let fallback = '';
32
+ for (const rawLine of source.split(/\r?\n/)) {
33
+ const line = rawLine.trim();
34
+ const marker = /^(`{3,}|~{3,})(.*)$/.exec(line);
35
+ if (marker) {
36
+ if (!fence) {
37
+ fence = marker[1];
38
+ const language = marker[2].trim().match(/^[A-Za-z0-9_+-]{1,24}\b/)?.[0] || 'code';
39
+ fallback ||= `${language} snippet`;
40
+ } else if (marker[1][0] === fence[0] && marker[1].length >= fence.length && !marker[2].trim()) {
41
+ fence = null;
42
+ }
43
+ continue;
44
+ }
45
+ if (fence || !line || /^>/.test(line)) continue;
46
+ const withoutContext = line.replace(FILE_TOKEN, ' ').replace(/@(context|compile-log)\b/g, ' ').trim();
47
+ const cleaned = cleanLine(line);
48
+ if (!withoutContext || /^https?:\/\/\S+$/i.test(line)) {
49
+ fallback ||= cleaned;
50
+ continue;
51
+ }
52
+ for (const sentence of cleaned.split(/[\u3002\uff01\uff1f]+/u)) {
53
+ const candidate = sentence.trim().replace(/[,;:\uff0c\uff1b\uff1a]+$/u, '').trim();
54
+ if (!/[\p{L}\p{N}]/u.test(candidate) || isScaffolding(candidate)) continue;
55
+ fallback ||= candidate;
56
+ if (!isGeneric(candidate)) return shorten(candidate, maxChars);
57
+ }
58
+ }
59
+ return shorten(fallback, maxChars);
60
+ }
61
+
62
+ function cleanLine(value) {
63
+ return value
64
+ .replace(/^#{1,6}\s+/, '')
65
+ .replace(/^(?:[-*+]\s+|\d+\.\s+|\d+[)\u3001]\s*)/, '')
66
+ .replace(FILE_TOKEN, (_match, quoted, singleQuoted, bare) =>
67
+ String(quoted || singleQuoted || bare || '').replace(/\\/g, '/').split('/').pop())
68
+ .replace(/@(context|compile-log)\b/g, ' ')
69
+ .replace(/!?\[([^\]]*)\]\([^\n)]*\)/g, '$1')
70
+ .replace(/`([^`]+)`/g, '$1')
71
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
72
+ .replace(/https?:\/\/\S+/gi, address => {
73
+ try { return new URL(address).hostname; } catch (_error) { return ''; }
74
+ })
75
+ .replace(/\[(?:REDACTED_SECRET|local path(?::\d+)?)\]/gi, ' ')
76
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
77
+ .replace(/\s+/g, ' ').trim()
78
+ .replace(/^(?:\u8bf7(?:\u4f60)?\u5e2e\u6211|\u8bf7\u4f60|\u5e2e\u6211|\u9ebb\u70e6\u4f60(?:\u5e2e\u6211)?|\u9ebb\u70e6\u5e2e\u6211|please\s+)\s*/i, '');
79
+ }
80
+
81
+ function isScaffolding(value) {
82
+ return /^(?:my request(?: for [^:]+)?|request|task|context|files(?: mentioned by [^:]+)?|attachments|\u6211\u7684\u8bf7\u6c42|\u9700\u6c42|\u4efb\u52a1|\u4e0a\u4e0b\u6587|\u9644\u4ef6|\u6587\u4ef6)$/i.test(value);
83
+ }
84
+
85
+ function isGeneric(value) {
86
+ const text = String(value || '').trim().replace(/[\s.!?\u3002\uff1f\uff01\u2026]+$/u, '');
87
+ if (!text || Array.from(text).length < 2) return true;
88
+ return /^(?:ok(?:ay)?|yes|no|go|done|continue|proceed|thanks?|thank you|hello|hi|test|new task|untitled task|\u597d(?:\u7684)?|\u53ef\u4ee5|\u7ee7\u7eed|\u786e\u8ba4|\u786e\u5b9a|\u6536\u5230|\u8c22\u8c22|\u6d4b\u8bd5|\u6267\u884c|\u662f\u7684|\u4f60\u597d)$/i.test(text)
89
+ || /^[A-Za-z0-9_+-]{1,24} snippet$/i.test(text)
90
+ || /^[^\s]+\.(?:tex|bib|md|txt|pdf|svg|png|jpe?g|webp)(?::\d+)?$/i.test(text)
91
+ || /^(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}$/i.test(text);
92
+ }
93
+
94
+ function shorten(value, maxChars) {
95
+ const title = String(value || '').trim();
96
+ if (!title) return '';
97
+ const chars = Number.isFinite(maxChars) && maxChars > 0 ? Math.min(40, Math.floor(maxChars)) : 24;
98
+ const maxUnits = chars * 2;
99
+ const glyphs = segmenter
100
+ ? Array.from(segmenter.segment(title), item => item.segment) : Array.from(title);
101
+ const width = glyph => /[^\u0000-\u00ff]/u.test(glyph) ? 2 : 1;
102
+ if (title.length <= STORAGE_LIMIT && glyphs.reduce((total, glyph) => total + width(glyph), 0) <= maxUnits) return title;
103
+ let prefix = '';
104
+ let units = 0;
105
+ let index = 0;
106
+ for (; index < glyphs.length; index++) {
107
+ const glyph = glyphs[index];
108
+ if (units + width(glyph) > maxUnits - 2 || prefix.length + glyph.length > STORAGE_LIMIT - 1) break;
109
+ prefix += glyph;
110
+ units += width(glyph);
111
+ }
112
+ const boundary = prefix.lastIndexOf(' ');
113
+ if (/[A-Za-z0-9]$/.test(prefix) && /^[A-Za-z0-9]/.test(glyphs[index] || '') && boundary >= prefix.length * 0.6) {
114
+ prefix = prefix.slice(0, boundary);
115
+ }
116
+ return prefix.trimEnd().replace(/[,;:\uff0c\uff1b\uff1a]+$/u, '') + '\u2026';
117
+ }
118
+
119
+ return Object.freeze({ derive, fromPrompt, isGeneric });
120
+ });
@@ -0,0 +1,32 @@
1
+ (function (root, factory) {
2
+ const api = factory();
3
+ if (typeof module === 'object' && module.exports) module.exports = api;
4
+ if (root) root.CodexOverleafUpdateRuntimeIdentity = api;
5
+ })(typeof globalThis !== 'undefined' ? globalThis : this, function () {
6
+ 'use strict';
7
+
8
+ function version(value) { return /^\d+\.\d+\.\d+$/.test(String(value || '')) ? String(value) : ''; }
9
+ function compare(left, right) {
10
+ const a = left.split('.').map(Number);
11
+ const b = right.split('.').map(Number);
12
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
13
+ }
14
+
15
+ function inspectInstalledRuntime(status = {}, options = {}) {
16
+ const result = { installedVersion: version(status.activeVersion), nativeVersion: version(status.runtimeVersion),
17
+ extensionVersion: version(options.extensionVersion), runtimeVersion: version(options.runtimeVersion), state: 'unknown' };
18
+ if (!status.managed || !result.installedVersion || !result.nativeVersion ||
19
+ !result.extensionVersion || !result.runtimeVersion) return result;
20
+ if (['authorized', 'bound'].includes(status.authorization?.state) ||
21
+ ['staged', 'activation_pending', 'applying', 'awaiting_health'].includes(status.transaction?.state)) {
22
+ return { ...result, state: 'transaction_active' };
23
+ }
24
+ if (status.installedAligned !== true) return { ...result, state: 'repair_required' };
25
+ const loaded = [result.nativeVersion, result.extensionVersion, result.runtimeVersion];
26
+ if (loaded.every(value => value === result.installedVersion)) return { ...result, state: 'aligned' };
27
+ return { ...result, state: loaded.every(value => compare(result.installedVersion, value) >= 0)
28
+ ? 'reload_required' : 'repair_required' };
29
+ }
30
+
31
+ return Object.freeze({ inspectInstalledRuntime });
32
+ });
@@ -1581,7 +1581,7 @@
1581
1581
 
1582
1582
  #codex-overleaf-panel .codex-set-row-help {
1583
1583
  margin: 0;
1584
- color: var(--tl-fg-4);
1584
+ color: var(--tl-fg-3);
1585
1585
  font-size: 11px;
1586
1586
  line-height: 1.45;
1587
1587
  }
@@ -2170,7 +2170,7 @@
2170
2170
  padding: 4px 10px;
2171
2171
  font-size: 11px;
2172
2172
  cursor: pointer;
2173
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
2173
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
2174
2174
  animation: tl-fade-in 120ms ease-out;
2175
2175
  }
2176
2176
 
@@ -2185,6 +2185,32 @@
2185
2185
  border-color: var(--tl-border-strong);
2186
2186
  }
2187
2187
 
2188
+ /* Let the browser anchor backscroll while the reader is detached. */
2189
+ #codex-overleaf-panel [data-log][data-log-auto-follow="false"],
2190
+ #codex-overleaf-panel [data-main][data-log-auto-follow="false"] {
2191
+ overflow-anchor: auto;
2192
+ }
2193
+
2194
+ #codex-overleaf-panel :is(.run-copy-action, .run-fork-action, .tl-jump-latest, .run-final-answer__recovery-action):focus-visible {
2195
+ outline: 2px solid var(--tl-accent);
2196
+ outline-offset: 2px;
2197
+ }
2198
+
2199
+ #codex-overleaf-panel :is(input, textarea)::placeholder {
2200
+ color: var(--tl-fg-3);
2201
+ opacity: 1;
2202
+ }
2203
+
2204
+ .codex-provider-dialog-root .codex-provider-field :is(input, textarea)::placeholder {
2205
+ color: var(--provider-muted);
2206
+ opacity: 1;
2207
+ }
2208
+
2209
+ .codex-provider-dialog-root :is(button, input, textarea, select):focus-visible {
2210
+ outline: 2px solid var(--provider-accent);
2211
+ outline-offset: 2px;
2212
+ }
2213
+
2188
2214
  /* Active-session bar: the title on the left, rename + delete on the right, so
2189
2215
  the session you are working in can be renamed/deleted in place. */
2190
2216
  #codex-overleaf-panel .codex-thread-title {
@@ -3084,13 +3110,13 @@
3084
3110
  }
3085
3111
 
3086
3112
  #codex-overleaf-panel .run-final-answer .run-system-label {
3087
- color: var(--tl-accent);
3113
+ color: var(--tl-fg-3);
3088
3114
  font-weight: 650;
3089
3115
  letter-spacing: 0.01em;
3090
3116
  }
3091
3117
 
3092
3118
  #codex-overleaf-panel .run-completion-report[data-status="failed"] .run-system-label[data-system-label="conclusion"] {
3093
- color: var(--tl-fail);
3119
+ color: var(--tl-fg-3);
3094
3120
  }
3095
3121
 
3096
3122
  #codex-overleaf-panel .run-final-answer .run-change-summary {
@@ -3179,9 +3205,8 @@
3179
3205
  }
3180
3206
 
3181
3207
  #codex-overleaf-panel .run-final-answer__meta {
3182
- margin: 10px 0 0;
3183
- padding: 8px 0 0;
3184
- border-top: 1px solid var(--tl-border);
3208
+ margin: 14px 0 0;
3209
+ padding: 0;
3185
3210
  display: grid;
3186
3211
  grid-template-columns: max-content 1fr;
3187
3212
  column-gap: 12px;
@@ -3205,18 +3230,15 @@
3205
3230
  }
3206
3231
 
3207
3232
  #codex-overleaf-panel .run-completion-report[data-status="failed"] {
3208
- color: var(--tl-fail);
3233
+ color: var(--tl-fg-2);
3209
3234
  }
3210
3235
 
3211
- /* .run-final-answer resets color to fg-1 for normal reports; a failed report's
3212
- conclusion must keep the alert color or failure reads like success. */
3236
+ /* Failure severity belongs to the status header and compact notice. */
3213
3237
  #codex-overleaf-panel .run-completion-report[data-status="failed"] .run-final-answer {
3214
- color: var(--tl-fail);
3238
+ color: var(--tl-fg-2);
3215
3239
  }
3216
3240
 
3217
- /* Failed-status reports keep their alert color for the conclusion, but the
3218
- meta block stays muted so it still reads as system info rather than amplifying
3219
- the alert. */
3241
+ /* Keep system metadata subordinate to the answer in both themes. */
3220
3242
  #codex-overleaf-panel .run-completion-report[data-status="failed"] .run-final-answer__meta,
3221
3243
  #codex-overleaf-panel .run-completion-report[data-status="failed"] .run-final-answer__meta-value {
3222
3244
  color: var(--tl-fg-3);
@@ -3252,6 +3274,43 @@
3252
3274
  opacity: 0.6;
3253
3275
  }
3254
3276
 
3277
+ #codex-overleaf-panel .run-feedback {
3278
+ display: grid;
3279
+ gap: 5px;
3280
+ margin: 12px 0 0;
3281
+ padding: 10px 12px;
3282
+ border-radius: 8px;
3283
+ background: var(--tl-surface-2);
3284
+ color: var(--tl-fg-2);
3285
+ font-size: 12px;
3286
+ line-height: 1.5;
3287
+ overflow-wrap: anywhere;
3288
+ }
3289
+
3290
+ #codex-overleaf-panel .run-feedback p { margin: 0; }
3291
+ #codex-overleaf-panel .run-feedback__title { color: var(--tl-fail); font-weight: 650; }
3292
+ #codex-overleaf-panel .run-feedback[data-tone="warning"] .run-feedback__title { color: var(--tl-review); }
3293
+ #codex-overleaf-panel .run-feedback .run-final-answer__recovery-action {
3294
+ justify-self: start;
3295
+ margin: 3px 0 0;
3296
+ padding: 3px 0;
3297
+ border: 0;
3298
+ background: transparent;
3299
+ color: var(--tl-accent);
3300
+ font-weight: 600;
3301
+ }
3302
+
3303
+ #codex-overleaf-panel :is(.run-copy-action, .run-fork-action) {
3304
+ color: var(--tl-fg-3);
3305
+ }
3306
+
3307
+ @media (prefers-reduced-motion: reduce) {
3308
+ #codex-overleaf-panel :is(.tl-jump-latest, .run-copy-action, .run-fork-action, .run-final-answer__recovery-action) {
3309
+ animation: none;
3310
+ transition: none;
3311
+ }
3312
+ }
3313
+
3255
3314
  #codex-overleaf-panel .codex-probe-line {
3256
3315
  flex: 0 0 auto;
3257
3316
  min-height: 18px;
@@ -712,6 +712,22 @@ function getMirrorStatus(projectId, options = {}) {
712
712
  };
713
713
  }
714
714
 
715
+ function normalizeObservedPatchTime(value, fallback) {
716
+ const candidate = value == null ? fallback : value;
717
+ const timestamp = typeof candidate === 'number'
718
+ ? candidate
719
+ : typeof candidate === 'string' ? Date.parse(candidate) : NaN;
720
+ return Number.isFinite(timestamp) && Math.abs(timestamp) <= 8640000000000000
721
+ ? new Date(timestamp).toISOString()
722
+ : '';
723
+ }
724
+
725
+ function isFreshOtObservation(freshness) {
726
+ const timestamp = Date.parse(freshness?.observedAt ?? freshness?.lastPatchAt ?? '');
727
+ const ageMs = Date.now() - timestamp;
728
+ return Number.isFinite(timestamp) && ageMs >= 0 && ageMs <= 30000;
729
+ }
730
+
715
731
  function buildOtStatusFields(baseline = {}, trusted) {
716
732
  const metadata = {
717
733
  lastOtPatchAt: baseline.lastOtPatchAt || '',
@@ -727,13 +743,15 @@ function buildOtStatusFields(baseline = {}, trusted) {
727
743
  }
728
744
  const textFiles = (baseline.files || []).filter(file => file?.kind === 'text');
729
745
  const freshFiles = textFiles
730
- .filter(file => file.freshness?.source === 'ot' && file.freshness?.state === 'fresh')
746
+ .filter(file => file.freshness?.source === 'ot' && file.freshness?.state === 'fresh'
747
+ && isFreshOtObservation(file.freshness))
731
748
  .map(file => ({
732
749
  path: file.path,
733
750
  source: file.freshness.source,
734
751
  state: file.freshness.state,
735
752
  lastFullSyncAt: file.freshness.lastFullSyncAt || '',
736
753
  lastPatchAt: file.freshness.lastPatchAt || '',
754
+ observedAt: file.freshness.observedAt ?? file.freshness.lastPatchAt ?? '',
737
755
  observedVersion: file.freshness.observedVersion ?? null
738
756
  }))
739
757
  .sort((left, right) => left.path.localeCompare(right.path));
@@ -935,6 +953,7 @@ async function patchMirrorFiles({ projectId, files, rootDir, source = 'ot' }) {
935
953
  state: 'fresh',
936
954
  lastFullSyncAt: baseline.lastFullSyncAt || '',
937
955
  lastPatchAt: patchBatchAt,
956
+ observedAt: normalizeObservedPatchTime(patch.observedAt, patchBatchAt),
938
957
  observedVersion: patch.observedVersion ?? null
939
958
  }
940
959
  };
@@ -0,0 +1,177 @@
1
+ 'use strict';
2
+
3
+ const http = require('node:http');
4
+ const https = require('node:https');
5
+ const tls = require('node:tls');
6
+ const net = require('node:net');
7
+ const { Readable } = require('node:stream');
8
+ const { execFileSync } = require('node:child_process');
9
+ const { updateError } = require('./updateTrust');
10
+ let systemCache = { platform: '', at: 0, text: '' };
11
+
12
+ function proxyError(code, message, retryable = false) {
13
+ return Object.assign(updateError(code, message), { retryable });
14
+ }
15
+
16
+ function bypassProxy(url, values = '') {
17
+ const host = url.hostname.toLowerCase();
18
+ const port = url.port || (url.protocol === 'https:' ? '443' : '80');
19
+ return String(values).split(/[,;\s]+/).some(raw => {
20
+ if (!raw) return false;
21
+ if (raw === '*') return true;
22
+ if (raw === '<local>') return !host.includes('.');
23
+ const match = raw.toLowerCase().match(/^(.*?)(?::(\d+))?$/);
24
+ if (match[2] && match[2] !== port) return false;
25
+ const domain = match[1].replace(/^\*?\./, '');
26
+ return host === domain || host.endsWith('.' + domain);
27
+ });
28
+ }
29
+
30
+ function readSystemProxy(platform) {
31
+ if (systemCache.platform === platform && Date.now() - systemCache.at < 30000) return systemCache.text;
32
+ let text = '';
33
+ try {
34
+ const command = platform === 'darwin' ? '/usr/sbin/scutil' : 'reg.exe';
35
+ const args = platform === 'darwin' ? ['--proxy']
36
+ : ['query', 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings'];
37
+ text = execFileSync(command, args, { encoding: 'utf8', timeout: 1000, maxBuffer: 32768,
38
+ windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
39
+ } catch (_error) { /* environment proxies and direct access remain available */ }
40
+ systemCache = { platform, at: Date.now(), text };
41
+ return text;
42
+ }
43
+
44
+ function resolveReleaseProxy(value, options = {}) {
45
+ const url = new URL(value);
46
+ const env = options.env || process.env;
47
+ if (bypassProxy(url, env.NO_PROXY || env.no_proxy)) return null;
48
+ const protocol = url.protocol === 'https:' ? 'HTTPS' : 'HTTP';
49
+ let configured = env[protocol + '_PROXY'] || env[protocol.toLowerCase() + '_proxy'] || env.ALL_PROXY || env.all_proxy;
50
+ let source = 'environment';
51
+ const platform = options.platform || process.platform;
52
+ if (!configured && ['darwin', 'win32'].includes(platform)) {
53
+ source = 'system';
54
+ const text = (options.systemProxy || readSystemProxy)(platform);
55
+ if (platform === 'darwin') {
56
+ const field = key => text.match(new RegExp('(?:^|\\n)\\s*' + key + '\\s*:\\s*([^\\n]+)'))?.[1]?.trim() || '';
57
+ if (field(protocol + 'Enable') === '1') {
58
+ const host = field(protocol + 'Proxy');
59
+ const port = Number(field(protocol + 'Port'));
60
+ if (host && Number.isInteger(port) && port > 0 && port <= 65535) {
61
+ configured = 'http://' + (host.includes(':') ? '[' + host + ']' : host) + ':' + port;
62
+ }
63
+ }
64
+ const exceptions = text.match(/ExceptionsList\s*:[\s\S]*?\n\s*}/)?.[0] || '';
65
+ const values = Array.from(exceptions.matchAll(/\d+\s*:\s*([^\n]+)/g), match => match[1].trim()).join(',');
66
+ if (bypassProxy(url, values)) return null;
67
+ } else if (/ProxyEnable\s+REG_DWORD\s+0x1\b/i.test(text)) {
68
+ const server = text.match(/ProxyServer\s+REG_SZ\s+([^\r\n]+)/i)?.[1]?.trim() || '';
69
+ const exceptions = text.match(/ProxyOverride\s+REG_SZ\s+([^\r\n]+)/i)?.[1] || '';
70
+ if (bypassProxy(url, exceptions)) return null;
71
+ configured = server.includes('=')
72
+ ? server.split(';').find(part => part.toLowerCase().startsWith(protocol.toLowerCase() + '='))?.split('=').slice(1).join('=')
73
+ : server;
74
+ if (configured && !configured.includes('://')) configured = 'http://' + configured;
75
+ }
76
+ }
77
+ if (!configured) return null;
78
+ let proxy;
79
+ try { proxy = new URL(configured); } catch (_error) {
80
+ throw proxyError('update_proxy_invalid', 'The configured update proxy address is invalid.');
81
+ }
82
+ if (!['http:', 'https:'].includes(proxy.protocol)) {
83
+ throw proxyError('update_proxy_unsupported', 'The update proxy must use HTTP or HTTPS. SOCKS and PAC require an HTTP proxy endpoint.');
84
+ }
85
+ return { url: proxy.href, source };
86
+ }
87
+
88
+ function hostname(url) { return url.hostname.replace(/^\[|\]$/g, ''); }
89
+ function proxyHeaders(proxy) {
90
+ if (!proxy.username && !proxy.password) return {};
91
+ let credentials;
92
+ try { credentials = decodeURIComponent(proxy.username) + ':' + decodeURIComponent(proxy.password); }
93
+ catch (_error) { throw proxyError('update_proxy_invalid', 'The configured proxy credentials are malformed.'); }
94
+ return { 'Proxy-Authorization': 'Basic ' + Buffer.from(credentials).toString('base64') };
95
+ }
96
+
97
+ function tunnel(target, proxy, signal) {
98
+ return new Promise((resolve, reject) => {
99
+ const authority = target.hostname + ':' + (target.port || '443');
100
+ const client = proxy.protocol === 'https:' ? https : http;
101
+ const request = client.request({ protocol: proxy.protocol, hostname: hostname(proxy), port: proxy.port,
102
+ method: 'CONNECT', path: authority, headers: { Host: authority, ...proxyHeaders(proxy) }, agent: false, signal });
103
+ request.once('error', reject);
104
+ request.once('connect', (response, socket, head) => {
105
+ if (response.statusCode !== 200) {
106
+ socket.destroy();
107
+ reject(proxyError(response.statusCode === 407 ? 'update_proxy_auth_required' : 'update_proxy_failed',
108
+ 'The proxy refused the secure update connection (HTTP ' + response.statusCode + ').', response.statusCode >= 500));
109
+ return;
110
+ }
111
+ if (signal.aborted) { socket.destroy(); reject(signal.reason); return; }
112
+ if (head.length) socket.unshift(head);
113
+ const secure = tls.connect({ socket, host: hostname(target),
114
+ servername: net.isIP(hostname(target)) ? undefined : hostname(target), ALPNProtocols: ['http/1.1'] });
115
+ const abort = () => secure.destroy(signal.reason);
116
+ signal.addEventListener('abort', abort, { once: true });
117
+ secure.once('close', () => signal.removeEventListener('abort', abort));
118
+ secure.once('error', reject);
119
+ secure.once('secureConnect', () => resolve(secure));
120
+ });
121
+ request.end();
122
+ });
123
+ }
124
+
125
+ async function proxyRequest(target, proxy, init) {
126
+ let agent;
127
+ if (target.protocol === 'https:') {
128
+ const socket = await tunnel(target, proxy, init.signal);
129
+ if (init.signal.aborted) { socket.destroy(); throw init.signal.reason; }
130
+ agent = new https.Agent({ keepAlive: false });
131
+ agent.createConnection = () => socket;
132
+ }
133
+ return new Promise((resolve, reject) => {
134
+ const headers = { ...Object.fromEntries(new Headers(init.headers || {})), Host: target.host };
135
+ const secure = target.protocol === 'https:';
136
+ const client = secure || proxy.protocol === 'https:' ? https : http;
137
+ const request = secure
138
+ ? client.request(target, { method: init.method, headers, agent, signal: init.signal })
139
+ : client.request({ protocol: proxy.protocol, hostname: hostname(proxy), port: proxy.port,
140
+ method: init.method, path: target.href, headers: { ...headers, ...proxyHeaders(proxy) }, agent: false, signal: init.signal });
141
+ request.once('error', error => { agent?.destroy(); reject(error); });
142
+ request.once('response', incoming => {
143
+ incoming.once('close', () => agent?.destroy());
144
+ const headers = new Headers();
145
+ for (let i = 0; i < incoming.rawHeaders.length; i += 2) headers.append(incoming.rawHeaders[i], incoming.rawHeaders[i + 1]);
146
+ const empty = init.method === 'HEAD' || [204, 205, 304].includes(incoming.statusCode);
147
+ if (empty) incoming.resume();
148
+ const response = new Response(empty ? null : Readable.toWeb(incoming),
149
+ { status: incoming.statusCode, statusText: incoming.statusMessage, headers });
150
+ Object.defineProperty(response, 'url', { value: target.href });
151
+ resolve(response);
152
+ });
153
+ request.end();
154
+ });
155
+ }
156
+
157
+ async function fetchRelease(url, init, options = {}) {
158
+ const proxy = resolveReleaseProxy(url, options);
159
+ if (!proxy) return globalThis.fetch(url, init);
160
+ let target = new URL(url);
161
+ for (let redirect = 0; redirect <= 5; redirect += 1) {
162
+ const response = await proxyRequest(target, new URL(proxy.url), init);
163
+ if (![301, 302, 303, 307, 308].includes(response.status)) return response;
164
+ const location = response.headers.get('location');
165
+ if (!location) return response;
166
+ if (response.body) await response.body.cancel();
167
+ const next = new URL(location, target);
168
+ if (!['http:', 'https:'].includes(next.protocol) || target.protocol === 'https:' && next.protocol !== 'https:' ||
169
+ options.allowedHosts && !options.allowedHosts.has(next.hostname)) {
170
+ throw proxyError('update_asset_redirect_forbidden', 'Release request redirected to an untrusted host.');
171
+ }
172
+ target = next;
173
+ }
174
+ throw proxyError('update_network_redirect_limit', 'The update request exceeded its redirect limit.');
175
+ }
176
+
177
+ module.exports = { fetchRelease, resolveReleaseProxy };