codex-overleaf-link 2.3.5 → 2.4.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.
- package/README.md +63 -31
- package/README.zh-CN.md +543 -0
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +7 -0
- package/extension/src/backgroundUpdateCoordinator.js +88 -4
- package/extension/src/content/activeTurnControl.js +1 -0
- package/extension/src/content/assetTransferBroker.js +2 -0
- package/extension/src/content/contentRuntime.js +77 -57
- package/extension/src/content/contextTray.js +4 -0
- package/extension/src/content/diagnosticsController.js +17 -3
- package/extension/src/content/diagnosticsPanel.js +129 -5
- package/extension/src/content/generated/content.bundle.js +1422 -348
- package/extension/src/content/generated/content.bundle.meta.json +7 -3
- package/extension/src/content/legacyGlobalRegistry.js +3 -0
- package/extension/src/content/modelPicker.js +21 -9
- package/extension/src/content/modelPickerSupport.js +38 -1
- package/extension/src/content/nativeCompatibilityController.js +4 -1
- package/extension/src/content/otWarmMirror.js +126 -92
- package/extension/src/content/otWarmMirrorController.js +122 -1
- package/extension/src/content/pageBridgeClient.js +2 -0
- 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/runResultActions.js +16 -1
- package/extension/src/content/runScrollLayout.js +124 -0
- package/extension/src/content/runTimelineView.js +37 -11
- package/extension/src/content/sessionForkController.js +20 -11
- package/extension/src/content/settingsPanel.js +28 -11
- package/extension/src/content/trackedChangeCaptureController.js +153 -0
- package/extension/src/content/updateNotice.js +24 -4
- package/extension/src/content/writebackOrchestrator.js +31 -0
- package/extension/src/page/binaryAssetUploader.js +101 -29
- package/extension/src/page/overleafRealtimeObserver.js +120 -25
- package/extension/src/page/textFileCreator.js +493 -0
- package/extension/src/page/trackedChangeCapture.js +342 -0
- package/extension/src/page/trackedChangesLifecycle.js +145 -149
- package/extension/src/page/treeOperations.js +30 -2
- package/extension/src/page/writebackRouter.js +94 -48
- package/extension/src/pageBridge.js +62 -25
- 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/pageRpcContract.js +2 -1
- package/extension/src/shared/sessionState.js +23 -23
- package/extension/src/shared/sessionTitle.js +120 -0
- package/extension/src/shared/storageDb.js +10 -12
- package/extension/src/shared/storageRunActions.js +29 -1
- package/extension/src/shared/updateRuntimeIdentity.js +32 -0
- package/extension/src/shared/writebackSettlement.js +19 -5
- package/extension/styles/panel.css +128 -24
- 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/taskRunnerRuntime.js +25 -0
- package/native-host/src/updateManager.js +40 -39
- package/package.json +5 -1
|
@@ -1029,11 +1029,21 @@
|
|
|
1029
1029
|
}
|
|
1030
1030
|
|
|
1031
1031
|
#codex-overleaf-panel .codex-diagnostics-menu {
|
|
1032
|
-
position:
|
|
1032
|
+
position: fixed;
|
|
1033
1033
|
z-index: 2147483647;
|
|
1034
|
-
top:
|
|
1035
|
-
right:
|
|
1036
|
-
|
|
1034
|
+
top: 44px;
|
|
1035
|
+
right: 12px;
|
|
1036
|
+
bottom: auto;
|
|
1037
|
+
left: auto;
|
|
1038
|
+
margin: 0;
|
|
1039
|
+
box-sizing: border-box;
|
|
1040
|
+
width: min(300px, calc(100vw - 24px));
|
|
1041
|
+
max-width: calc(100vw - 24px);
|
|
1042
|
+
max-height: calc(100vh - 24px);
|
|
1043
|
+
overflow: auto;
|
|
1044
|
+
overscroll-behavior: contain;
|
|
1045
|
+
overflow-wrap: anywhere;
|
|
1046
|
+
color: var(--tl-fg-2);
|
|
1037
1047
|
border: 1px solid var(--tl-border-strong);
|
|
1038
1048
|
border-radius: 6px;
|
|
1039
1049
|
background: var(--tl-surface-2);
|
|
@@ -1084,15 +1094,30 @@
|
|
|
1084
1094
|
z-index: 2147483647;
|
|
1085
1095
|
top: 44px;
|
|
1086
1096
|
right: 12px;
|
|
1087
|
-
|
|
1088
|
-
|
|
1097
|
+
bottom: auto;
|
|
1098
|
+
left: auto;
|
|
1099
|
+
margin: 0;
|
|
1100
|
+
padding: 0;
|
|
1101
|
+
box-sizing: border-box;
|
|
1102
|
+
width: min(560px, calc(100vw - 24px));
|
|
1103
|
+
max-width: calc(100vw - 24px);
|
|
1104
|
+
max-height: min(640px, calc(100vh - 24px));
|
|
1089
1105
|
overflow: auto;
|
|
1106
|
+
overscroll-behavior: contain;
|
|
1107
|
+
overflow-wrap: anywhere;
|
|
1108
|
+
color: var(--tl-fg-2);
|
|
1090
1109
|
border: 1px solid var(--tl-border-strong);
|
|
1091
1110
|
border-radius: 8px;
|
|
1092
1111
|
background: var(--tl-surface-1);
|
|
1093
1112
|
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.48);
|
|
1094
1113
|
}
|
|
1095
1114
|
|
|
1115
|
+
#codex-overleaf-panel .codex-diagnostics-menu::backdrop,
|
|
1116
|
+
#codex-overleaf-panel .codex-diagnostics-result::backdrop {
|
|
1117
|
+
background: transparent;
|
|
1118
|
+
pointer-events: none;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1096
1121
|
#codex-overleaf-panel .codex-diagnostics-result[hidden] {
|
|
1097
1122
|
display: none;
|
|
1098
1123
|
}
|
|
@@ -1581,7 +1606,7 @@
|
|
|
1581
1606
|
|
|
1582
1607
|
#codex-overleaf-panel .codex-set-row-help {
|
|
1583
1608
|
margin: 0;
|
|
1584
|
-
color: var(--tl-fg-
|
|
1609
|
+
color: var(--tl-fg-3);
|
|
1585
1610
|
font-size: 11px;
|
|
1586
1611
|
line-height: 1.45;
|
|
1587
1612
|
}
|
|
@@ -1821,9 +1846,12 @@
|
|
|
1821
1846
|
#codex-overleaf-panel .codex-plugin-confirm::backdrop { background: transparent; }
|
|
1822
1847
|
|
|
1823
1848
|
#codex-overleaf-panel .codex-plugin-confirm-card {
|
|
1849
|
+
display: flex;
|
|
1850
|
+
flex-direction: column;
|
|
1851
|
+
box-sizing: border-box;
|
|
1824
1852
|
width: 100%;
|
|
1825
1853
|
max-height: min(520px, calc(100vh - 28px));
|
|
1826
|
-
overflow:
|
|
1854
|
+
overflow: auto;
|
|
1827
1855
|
border: 1px solid var(--tl-border-strong);
|
|
1828
1856
|
border-radius: 8px;
|
|
1829
1857
|
background: var(--tl-surface-1);
|
|
@@ -1832,6 +1860,7 @@
|
|
|
1832
1860
|
|
|
1833
1861
|
#codex-overleaf-panel .codex-plugin-confirm-head {
|
|
1834
1862
|
display: flex;
|
|
1863
|
+
flex: 0 0 auto;
|
|
1835
1864
|
align-items: center;
|
|
1836
1865
|
gap: 10px;
|
|
1837
1866
|
padding: 14px 14px 12px;
|
|
@@ -1860,6 +1889,8 @@
|
|
|
1860
1889
|
}
|
|
1861
1890
|
|
|
1862
1891
|
#codex-overleaf-panel .codex-plugin-confirm-body {
|
|
1892
|
+
flex: 0 1 auto;
|
|
1893
|
+
min-height: 0;
|
|
1863
1894
|
max-height: 300px;
|
|
1864
1895
|
overflow: auto;
|
|
1865
1896
|
padding: 12px 14px;
|
|
@@ -1872,6 +1903,9 @@
|
|
|
1872
1903
|
|
|
1873
1904
|
#codex-overleaf-panel .codex-plugin-confirm-actions {
|
|
1874
1905
|
display: flex;
|
|
1906
|
+
flex: 0 0 auto;
|
|
1907
|
+
flex-wrap: wrap;
|
|
1908
|
+
align-items: stretch;
|
|
1875
1909
|
justify-content: flex-end;
|
|
1876
1910
|
gap: 8px;
|
|
1877
1911
|
padding: 12px 14px 14px;
|
|
@@ -1879,12 +1913,23 @@
|
|
|
1879
1913
|
}
|
|
1880
1914
|
|
|
1881
1915
|
#codex-overleaf-panel .codex-plugin-confirm-actions button {
|
|
1882
|
-
|
|
1883
|
-
|
|
1916
|
+
display: flex;
|
|
1917
|
+
flex: 1 1 auto;
|
|
1918
|
+
align-items: center;
|
|
1919
|
+
justify-content: center;
|
|
1920
|
+
box-sizing: border-box;
|
|
1921
|
+
min-width: min(72px, 100%);
|
|
1922
|
+
max-width: 100%;
|
|
1923
|
+
min-height: 34px;
|
|
1924
|
+
height: auto;
|
|
1884
1925
|
border: 1px solid var(--tl-border-strong);
|
|
1885
1926
|
border-radius: 6px;
|
|
1886
|
-
padding:
|
|
1927
|
+
padding: 7px 12px;
|
|
1887
1928
|
font-weight: 650;
|
|
1929
|
+
line-height: 1.35;
|
|
1930
|
+
text-align: center;
|
|
1931
|
+
white-space: normal;
|
|
1932
|
+
overflow-wrap: anywhere;
|
|
1888
1933
|
}
|
|
1889
1934
|
|
|
1890
1935
|
#codex-overleaf-panel .codex-plugin-confirm-cancel {
|
|
@@ -2170,7 +2215,7 @@
|
|
|
2170
2215
|
padding: 4px 10px;
|
|
2171
2216
|
font-size: 11px;
|
|
2172
2217
|
cursor: pointer;
|
|
2173
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.
|
|
2218
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
2174
2219
|
animation: tl-fade-in 120ms ease-out;
|
|
2175
2220
|
}
|
|
2176
2221
|
|
|
@@ -2185,6 +2230,32 @@
|
|
|
2185
2230
|
border-color: var(--tl-border-strong);
|
|
2186
2231
|
}
|
|
2187
2232
|
|
|
2233
|
+
/* Let the browser anchor backscroll while the reader is detached. */
|
|
2234
|
+
#codex-overleaf-panel [data-log][data-log-auto-follow="false"],
|
|
2235
|
+
#codex-overleaf-panel [data-main][data-log-auto-follow="false"] {
|
|
2236
|
+
overflow-anchor: auto;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
#codex-overleaf-panel :is(.run-copy-action, .run-fork-action, .tl-jump-latest, .run-final-answer__recovery-action):focus-visible {
|
|
2240
|
+
outline: 2px solid var(--tl-accent);
|
|
2241
|
+
outline-offset: 2px;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
#codex-overleaf-panel :is(input, textarea)::placeholder {
|
|
2245
|
+
color: var(--tl-fg-3);
|
|
2246
|
+
opacity: 1;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
.codex-provider-dialog-root .codex-provider-field :is(input, textarea)::placeholder {
|
|
2250
|
+
color: var(--provider-muted);
|
|
2251
|
+
opacity: 1;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
.codex-provider-dialog-root :is(button, input, textarea, select):focus-visible {
|
|
2255
|
+
outline: 2px solid var(--provider-accent);
|
|
2256
|
+
outline-offset: 2px;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2188
2259
|
/* Active-session bar: the title on the left, rename + delete on the right, so
|
|
2189
2260
|
the session you are working in can be renamed/deleted in place. */
|
|
2190
2261
|
#codex-overleaf-panel .codex-thread-title {
|
|
@@ -3084,13 +3155,13 @@
|
|
|
3084
3155
|
}
|
|
3085
3156
|
|
|
3086
3157
|
#codex-overleaf-panel .run-final-answer .run-system-label {
|
|
3087
|
-
color: var(--tl-
|
|
3158
|
+
color: var(--tl-fg-3);
|
|
3088
3159
|
font-weight: 650;
|
|
3089
3160
|
letter-spacing: 0.01em;
|
|
3090
3161
|
}
|
|
3091
3162
|
|
|
3092
3163
|
#codex-overleaf-panel .run-completion-report[data-status="failed"] .run-system-label[data-system-label="conclusion"] {
|
|
3093
|
-
color: var(--tl-
|
|
3164
|
+
color: var(--tl-fg-3);
|
|
3094
3165
|
}
|
|
3095
3166
|
|
|
3096
3167
|
#codex-overleaf-panel .run-final-answer .run-change-summary {
|
|
@@ -3179,9 +3250,8 @@
|
|
|
3179
3250
|
}
|
|
3180
3251
|
|
|
3181
3252
|
#codex-overleaf-panel .run-final-answer__meta {
|
|
3182
|
-
margin:
|
|
3183
|
-
padding:
|
|
3184
|
-
border-top: 1px solid var(--tl-border);
|
|
3253
|
+
margin: 14px 0 0;
|
|
3254
|
+
padding: 0;
|
|
3185
3255
|
display: grid;
|
|
3186
3256
|
grid-template-columns: max-content 1fr;
|
|
3187
3257
|
column-gap: 12px;
|
|
@@ -3205,18 +3275,15 @@
|
|
|
3205
3275
|
}
|
|
3206
3276
|
|
|
3207
3277
|
#codex-overleaf-panel .run-completion-report[data-status="failed"] {
|
|
3208
|
-
color: var(--tl-
|
|
3278
|
+
color: var(--tl-fg-2);
|
|
3209
3279
|
}
|
|
3210
3280
|
|
|
3211
|
-
/*
|
|
3212
|
-
conclusion must keep the alert color or failure reads like success. */
|
|
3281
|
+
/* Failure severity belongs to the status header and compact notice. */
|
|
3213
3282
|
#codex-overleaf-panel .run-completion-report[data-status="failed"] .run-final-answer {
|
|
3214
|
-
color: var(--tl-
|
|
3283
|
+
color: var(--tl-fg-2);
|
|
3215
3284
|
}
|
|
3216
3285
|
|
|
3217
|
-
/*
|
|
3218
|
-
meta block stays muted so it still reads as system info rather than amplifying
|
|
3219
|
-
the alert. */
|
|
3286
|
+
/* Keep system metadata subordinate to the answer in both themes. */
|
|
3220
3287
|
#codex-overleaf-panel .run-completion-report[data-status="failed"] .run-final-answer__meta,
|
|
3221
3288
|
#codex-overleaf-panel .run-completion-report[data-status="failed"] .run-final-answer__meta-value {
|
|
3222
3289
|
color: var(--tl-fg-3);
|
|
@@ -3252,6 +3319,43 @@
|
|
|
3252
3319
|
opacity: 0.6;
|
|
3253
3320
|
}
|
|
3254
3321
|
|
|
3322
|
+
#codex-overleaf-panel .run-feedback {
|
|
3323
|
+
display: grid;
|
|
3324
|
+
gap: 5px;
|
|
3325
|
+
margin: 12px 0 0;
|
|
3326
|
+
padding: 10px 12px;
|
|
3327
|
+
border-radius: 8px;
|
|
3328
|
+
background: var(--tl-surface-2);
|
|
3329
|
+
color: var(--tl-fg-2);
|
|
3330
|
+
font-size: 12px;
|
|
3331
|
+
line-height: 1.5;
|
|
3332
|
+
overflow-wrap: anywhere;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
#codex-overleaf-panel .run-feedback p { margin: 0; }
|
|
3336
|
+
#codex-overleaf-panel .run-feedback__title { color: var(--tl-fail); font-weight: 650; }
|
|
3337
|
+
#codex-overleaf-panel .run-feedback[data-tone="warning"] .run-feedback__title { color: var(--tl-review); }
|
|
3338
|
+
#codex-overleaf-panel .run-feedback .run-final-answer__recovery-action {
|
|
3339
|
+
justify-self: start;
|
|
3340
|
+
margin: 3px 0 0;
|
|
3341
|
+
padding: 3px 0;
|
|
3342
|
+
border: 0;
|
|
3343
|
+
background: transparent;
|
|
3344
|
+
color: var(--tl-accent);
|
|
3345
|
+
font-weight: 600;
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
#codex-overleaf-panel :is(.run-copy-action, .run-fork-action) {
|
|
3349
|
+
color: var(--tl-fg-3);
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3353
|
+
#codex-overleaf-panel :is(.tl-jump-latest, .run-copy-action, .run-fork-action, .run-final-answer__recovery-action) {
|
|
3354
|
+
animation: none;
|
|
3355
|
+
transition: none;
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3255
3359
|
#codex-overleaf-panel .codex-probe-line {
|
|
3256
3360
|
flex: 0 0 auto;
|
|
3257
3361
|
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 };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { updateError } = require('./updateTrust');
|
|
4
|
+
const { fetchRelease } = require('./releaseProxy');
|
|
5
|
+
|
|
6
|
+
const RETRYABLE_HTTP = new Set([408, 429, 500, 502, 503, 504]);
|
|
7
|
+
const CERTIFICATE_ERRORS = /CERT|TLS_CERT|SELF_SIGNED|UNABLE_TO_VERIFY|DEPTH_ZERO/;
|
|
8
|
+
|
|
9
|
+
function abortable(promise, signal) {
|
|
10
|
+
if (signal.aborted) {
|
|
11
|
+
void Promise.resolve(promise).catch(() => {});
|
|
12
|
+
return Promise.reject(signal.reason);
|
|
13
|
+
}
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const abort = () => reject(signal.reason);
|
|
16
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
17
|
+
Promise.resolve(promise).then(resolve, reject).finally(() => signal.removeEventListener('abort', abort));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function terminalError(code, message) {
|
|
22
|
+
return Object.assign(updateError(code, message), { retryable: false });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function readBoundedBody(response, limit, signal) {
|
|
26
|
+
const declared = Number(response.headers?.get?.('content-length') || 0);
|
|
27
|
+
if (declared > limit) throw terminalError('update_download_limit', 'Update response exceeds its size limit.');
|
|
28
|
+
const reader = response.body?.getReader?.();
|
|
29
|
+
if (!reader) {
|
|
30
|
+
const bytes = Buffer.from(await abortable(response.arrayBuffer(), signal));
|
|
31
|
+
if (!bytes.length || bytes.length > limit) throw terminalError('update_download_limit', 'Update response exceeds its size limit.');
|
|
32
|
+
return bytes;
|
|
33
|
+
}
|
|
34
|
+
const chunks = [];
|
|
35
|
+
let length = 0;
|
|
36
|
+
try {
|
|
37
|
+
while (true) {
|
|
38
|
+
const chunk = await abortable(reader.read(), signal);
|
|
39
|
+
if (chunk.done) break;
|
|
40
|
+
const bytes = Buffer.from(chunk.value);
|
|
41
|
+
length += bytes.length;
|
|
42
|
+
if (length > limit) throw terminalError('update_download_limit', 'Update response exceeds its size limit.');
|
|
43
|
+
chunks.push(bytes);
|
|
44
|
+
}
|
|
45
|
+
if (!length) throw terminalError('update_download_limit', 'Update response is empty.');
|
|
46
|
+
return Buffer.concat(chunks, length);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
void reader.cancel().catch(() => {});
|
|
49
|
+
throw error;
|
|
50
|
+
} finally {
|
|
51
|
+
reader.releaseLock();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function networkFailure(error, signal, callerSignal) {
|
|
56
|
+
let cause = error;
|
|
57
|
+
for (let depth = 0; depth < 8 && cause?.cause && cause.cause !== cause; depth += 1) cause = cause.cause;
|
|
58
|
+
const reason = String(typeof cause?.code === 'string' ? cause.code : cause?.name || error?.name || 'network_error');
|
|
59
|
+
if (callerSignal?.aborted) return { code: 'update_network_cancelled', reason: 'cancelled', retryable: false };
|
|
60
|
+
if (error?.retryable === false) return { code: error.code || 'update_network_failed', reason, retryable: false };
|
|
61
|
+
if (CERTIFICATE_ERRORS.test(reason)) return { code: 'update_network_certificate', reason, retryable: false };
|
|
62
|
+
if (signal.aborted || /TIMEOUT|Timeout|AbortError/.test(reason)) {
|
|
63
|
+
return { code: 'update_network_timeout', reason, retryable: true };
|
|
64
|
+
}
|
|
65
|
+
if (/ENOTFOUND|EAI_AGAIN/.test(reason)) return { code: 'update_network_dns', reason, retryable: true };
|
|
66
|
+
return { code: /^update_[a-z0-9_]+$/.test(error?.code || '') ? error.code : 'update_network_failed',
|
|
67
|
+
reason, retryable: error?.retryable !== false };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function retryDelay(response, fallback) {
|
|
71
|
+
const value = response?.headers?.get?.('retry-after');
|
|
72
|
+
if (!value) return fallback;
|
|
73
|
+
const seconds = Number(value);
|
|
74
|
+
const delay = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(value) - Date.now();
|
|
75
|
+
return Number.isFinite(delay) ? Math.max(fallback, delay, 0) : fallback;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function delay(milliseconds, signal) {
|
|
79
|
+
if (!milliseconds) return Promise.resolve();
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const abort = () => { clearTimeout(timer); reject(signal.reason); };
|
|
82
|
+
const timer = setTimeout(() => { signal?.removeEventListener('abort', abort); resolve(); }, milliseconds);
|
|
83
|
+
if (signal?.aborted) abort();
|
|
84
|
+
else signal?.addEventListener('abort', abort, { once: true });
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function requestRelease(url, options = {}) {
|
|
89
|
+
const started = Date.now();
|
|
90
|
+
const deadline = options.deadlineAt || started + (options.totalTimeoutMs ?? 45000);
|
|
91
|
+
const maxAttempts = Math.max(1, Math.min(5, options.maxAttempts ?? 3));
|
|
92
|
+
const host = new URL(url).hostname;
|
|
93
|
+
let failure;
|
|
94
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
95
|
+
const remaining = deadline - Date.now();
|
|
96
|
+
if (remaining <= 0) break;
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const timer = setTimeout(() => controller.abort(new DOMException('Update request timed out.', 'TimeoutError')),
|
|
99
|
+
Math.min(remaining, options.timeoutMs ?? 12000));
|
|
100
|
+
const cancel = () => controller.abort(options.signal.reason);
|
|
101
|
+
if (options.signal?.aborted) cancel();
|
|
102
|
+
else options.signal?.addEventListener('abort', cancel, { once: true });
|
|
103
|
+
let response;
|
|
104
|
+
let retryAfter = 0;
|
|
105
|
+
try {
|
|
106
|
+
const fetchImpl = options.fetch || ((value, init) => fetchRelease(value, init, options));
|
|
107
|
+
response = await abortable(fetchImpl(url, {
|
|
108
|
+
method: options.method || 'GET', headers: options.headers,
|
|
109
|
+
redirect: 'follow', signal: controller.signal
|
|
110
|
+
}), controller.signal);
|
|
111
|
+
const finalUrl = new URL(response.url || url);
|
|
112
|
+
if (options.allowedHosts && (finalUrl.protocol !== 'https:' || !options.allowedHosts.has(finalUrl.hostname))) {
|
|
113
|
+
throw terminalError('update_asset_redirect_forbidden', 'Release request redirected to an untrusted host.');
|
|
114
|
+
}
|
|
115
|
+
if (!response.ok && response.status !== 304) {
|
|
116
|
+
throw Object.assign(updateError(options.httpErrorCode || 'update_asset_http_error',
|
|
117
|
+
'Update request failed with HTTP ' + response.status + '.'), { retryable: RETRYABLE_HTTP.has(response.status) });
|
|
118
|
+
}
|
|
119
|
+
const bytes = options.method === 'HEAD' || response.status === 304
|
|
120
|
+
? null : await readBoundedBody(response, options.limit ?? 32 * 1024 * 1024, controller.signal);
|
|
121
|
+
return { ok: response.ok, status: response.status, url: finalUrl.href, headers: response.headers,
|
|
122
|
+
bytes, attempts: attempt, elapsedMs: Date.now() - started };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
const classified = networkFailure(error, controller.signal, options.signal);
|
|
125
|
+
const diagnostics = { stage: options.stage || 'download', host, attempt,
|
|
126
|
+
elapsedMs: Date.now() - started, reason: classified.reason, retryable: classified.retryable,
|
|
127
|
+
...(response ? { httpStatus: response.status } : {}) };
|
|
128
|
+
const description = classified.code === 'update_network_timeout' ? 'Connection or download timed out'
|
|
129
|
+
: classified.code === 'update_network_dns' ? 'DNS lookup failed'
|
|
130
|
+
: classified.code === 'update_network_certificate' ? 'TLS certificate verification failed'
|
|
131
|
+
: classified.code === 'update_network_cancelled' ? 'Update request was cancelled'
|
|
132
|
+
: error?.retryable === false ? error.message : 'Network request failed';
|
|
133
|
+
const reason = response ? 'HTTP ' + response.status
|
|
134
|
+
: /^[A-Z][A-Z0-9_]{1,79}$/.test(classified.reason) ? classified.reason : '';
|
|
135
|
+
failure = updateError(classified.code,
|
|
136
|
+
description.replace(/\.$/, '') + ' (' + diagnostics.stage + ', ' + host + ', attempt ' + attempt +
|
|
137
|
+
(reason ? ', ' + reason : '') + ', ' + (diagnostics.elapsedMs / 1000).toFixed(1) + 's).',
|
|
138
|
+
{ cause: error, network: diagnostics });
|
|
139
|
+
try { options.onDiagnostic?.(diagnostics); } catch (_error) { /* diagnostics are best effort */ }
|
|
140
|
+
if (!classified.retryable || attempt === maxAttempts) throw failure;
|
|
141
|
+
retryAfter = retryDelay(response, (options.delayMs ?? 500) * attempt);
|
|
142
|
+
} finally {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
options.signal?.removeEventListener('abort', cancel);
|
|
145
|
+
controller.abort();
|
|
146
|
+
if (response?.body && !response.body.locked) void response.body.cancel().catch(() => {});
|
|
147
|
+
}
|
|
148
|
+
if (Date.now() + retryAfter >= deadline) break;
|
|
149
|
+
await delay(retryAfter, options.signal);
|
|
150
|
+
}
|
|
151
|
+
throw failure || updateError('update_network_timeout', 'The update network time budget was exhausted.');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = { requestRelease };
|
|
@@ -82,6 +82,10 @@ async function handleRequest(request, env = process.env, emit = () => {}) {
|
|
|
82
82
|
return handleMirrorStatus(request, env);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
if (request.method === 'mirror.invalidate') {
|
|
86
|
+
return handleMirrorInvalidate(request, env);
|
|
87
|
+
}
|
|
88
|
+
|
|
85
89
|
if (request.method === 'mirror.scanSensitive') {
|
|
86
90
|
return handleMirrorScanSensitive(request, env);
|
|
87
91
|
}
|
|
@@ -709,6 +713,27 @@ async function handleMirrorConfirmWriteback(request, env) {
|
|
|
709
713
|
}
|
|
710
714
|
}
|
|
711
715
|
|
|
716
|
+
function handleMirrorInvalidate(request, env) {
|
|
717
|
+
const projectId = request.params?.projectId;
|
|
718
|
+
if (typeof projectId !== 'string' || !projectId.trim()) {
|
|
719
|
+
return errorResponse(request.id, 'invalid_project_id', 'Mirror invalidation requires an explicit project id.');
|
|
720
|
+
}
|
|
721
|
+
const projectKey = resolveProjectKey({ projectId });
|
|
722
|
+
const lockToken = acquireProjectLock(projectKey);
|
|
723
|
+
if (!lockToken) {
|
|
724
|
+
return errorResponse(request.id, 'project_locked', `Project ${projectKey} is currently in use by codex.run`);
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
const { markMirrorDirty } = require('./mirrorWorkspace');
|
|
728
|
+
markMirrorDirty({ projectId, rootDir: env.CODEX_OVERLEAF_MIRROR_ROOT, reason: 'overleaf_undo' });
|
|
729
|
+
return okResponse(request.id, { invalidated: true, projectKey });
|
|
730
|
+
} catch (error) {
|
|
731
|
+
return errorResponse(request.id, 'mirror_invalidation_failed', error.message);
|
|
732
|
+
} finally {
|
|
733
|
+
releaseProjectLock(projectKey, lockToken);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
712
737
|
function handleMirrorStatus(request, env) {
|
|
713
738
|
const { getMirrorStatus } = require('./mirrorWorkspace');
|
|
714
739
|
const params = request.params || {};
|