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.
- package/README.md +31 -24
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +4 -0
- package/extension/src/backgroundUpdateCoordinator.js +88 -4
- package/extension/src/content/contentRuntime.js +24 -7
- package/extension/src/content/diagnosticsController.js +13 -3
- package/extension/src/content/diagnosticsPanel.js +1 -1
- package/extension/src/content/generated/content.bundle.js +848 -245
- package/extension/src/content/generated/content.bundle.meta.json +6 -3
- package/extension/src/content/legacyGlobalRegistry.js +2 -0
- package/extension/src/content/otWarmMirror.js +95 -65
- package/extension/src/content/otWarmMirrorController.js +102 -1
- package/extension/src/content/panelMaintenance.js +26 -15
- package/extension/src/content/providerSettingsCoordinator.js +1 -0
- package/extension/src/content/runFailureNotice.js +104 -0
- package/extension/src/content/runScrollLayout.js +124 -0
- package/extension/src/content/runTimelineView.js +36 -10
- package/extension/src/content/settingsPanel.js +28 -11
- package/extension/src/content/updateNotice.js +24 -4
- package/extension/src/page/overleafRealtimeObserver.js +116 -24
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/i18n.js +77 -37
- package/extension/src/shared/managedUpdateProjection.js +1 -1
- package/extension/src/shared/otText.js +0 -1
- package/extension/src/shared/sessionState.js +17 -21
- package/extension/src/shared/sessionTitle.js +120 -0
- package/extension/src/shared/updateRuntimeIdentity.js +32 -0
- package/extension/styles/panel.css +73 -14
- package/native-host/src/mirrorWorkspace.js +20 -1
- package/native-host/src/releaseProxy.js +177 -0
- package/native-host/src/releaseTransport.js +154 -0
- package/native-host/src/updateManager.js +40 -39
- 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-
|
|
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.
|
|
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-
|
|
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-
|
|
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:
|
|
3183
|
-
padding:
|
|
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-
|
|
3233
|
+
color: var(--tl-fg-2);
|
|
3209
3234
|
}
|
|
3210
3235
|
|
|
3211
|
-
/*
|
|
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-
|
|
3238
|
+
color: var(--tl-fg-2);
|
|
3215
3239
|
}
|
|
3216
3240
|
|
|
3217
|
-
/*
|
|
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 };
|