@goodandready/dsh-context-lens 0.1.17 → 0.1.19
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/CHANGELOG.md +107 -0
- package/README.md +86 -112
- package/README.ru.md +95 -110
- package/README.zh.md +99 -4
- package/lib/client.js +207 -20
- package/lib/http.js +114 -0
- package/lib/index.js +121 -203
- package/lib/tokens/tracker.js +0 -4
- package/lib/tools.js +179 -0
- package/lib/updater.js +237 -0
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -6,6 +6,13 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
const NS = '@goodandready/dsh-context-lens';
|
|
7
7
|
|
|
8
8
|
const en = {
|
|
9
|
+
'update.available': 'Update available: v{latestVersion} (current: v{currentVersion})',
|
|
10
|
+
'update.btn': 'Update Now',
|
|
11
|
+
'update.updating': 'Updating…',
|
|
12
|
+
'update.done': 'Successfully updated to v{version}! Please restart DSH.',
|
|
13
|
+
'update.failed': 'Update failed: {error}',
|
|
14
|
+
'update.checking': 'Checking for updates…',
|
|
15
|
+
'update.up_to_date': 'Up to date',
|
|
9
16
|
title: 'Context Lens & Token Guard',
|
|
10
17
|
sub: 'AST compression, log filtering, token budget',
|
|
11
18
|
mode: 'Compression mode',
|
|
@@ -37,6 +44,13 @@ window.__ModuleLoader__.load({
|
|
|
37
44
|
};
|
|
38
45
|
|
|
39
46
|
const zh = {
|
|
47
|
+
'update.available': '发现新版本 v{latestVersion}(当前版本 v{currentVersion})',
|
|
48
|
+
'update.btn': '立即更新',
|
|
49
|
+
'update.updating': '正在更新…',
|
|
50
|
+
'update.done': '更新成功至 v{version}!请重启 DSH 服务生效。',
|
|
51
|
+
'update.failed': '更新失败:{error}',
|
|
52
|
+
'update.checking': '正在检查更新…',
|
|
53
|
+
'update.up_to_date': '已是最新版本',
|
|
40
54
|
title: 'Context Lens 上下文镜头与 Token 守卫',
|
|
41
55
|
sub: 'AST 代码精简、日志过滤与 Token 预算监控',
|
|
42
56
|
mode: '压缩模式',
|
|
@@ -108,12 +122,12 @@ window.__ModuleLoader__.load({
|
|
|
108
122
|
.cl-badge-ok {
|
|
109
123
|
border-color: var(--dsw-alias-state-success-primary, #10b981);
|
|
110
124
|
color: var(--dsw-alias-state-success-primary, #10b981);
|
|
111
|
-
background:
|
|
125
|
+
background: color-mix(in srgb, var(--dsw-alias-state-success-primary, #10b981) 8%, transparent);
|
|
112
126
|
}
|
|
113
127
|
.cl-badge-warn {
|
|
114
128
|
border-color: var(--dsw-alias-state-error-primary, #ef4444);
|
|
115
129
|
color: var(--dsw-alias-state-error-primary, #ef4444);
|
|
116
|
-
background:
|
|
130
|
+
background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 8%, transparent);
|
|
117
131
|
}
|
|
118
132
|
.cl-stat-grid {
|
|
119
133
|
display: grid;
|
|
@@ -211,7 +225,7 @@ window.__ModuleLoader__.load({
|
|
|
211
225
|
.cl-banner-warn {
|
|
212
226
|
padding: 10px 14px;
|
|
213
227
|
border-radius: 8px;
|
|
214
|
-
background:
|
|
228
|
+
background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 10%, transparent);
|
|
215
229
|
border: 1px solid var(--dsw-alias-state-error-primary, #ef4444);
|
|
216
230
|
color: var(--dsw-alias-state-error-primary, #ef4444);
|
|
217
231
|
font-size: 12px;
|
|
@@ -244,7 +258,7 @@ window.__ModuleLoader__.load({
|
|
|
244
258
|
.cl-error-box {
|
|
245
259
|
padding: 12px;
|
|
246
260
|
border-radius: 8px;
|
|
247
|
-
background:
|
|
261
|
+
background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 8%, transparent);
|
|
248
262
|
border: 1px solid var(--dsw-alias-state-error-primary, #ef4444);
|
|
249
263
|
color: var(--dsw-alias-state-error-primary, #ef4444);
|
|
250
264
|
font-size: 12px;
|
|
@@ -253,6 +267,18 @@ window.__ModuleLoader__.load({
|
|
|
253
267
|
document.head.appendChild(style);
|
|
254
268
|
}
|
|
255
269
|
|
|
270
|
+
function makeT(dict, fallback) {
|
|
271
|
+
return function t(key, vars) {
|
|
272
|
+
let val = (dict && dict[key]) || (fallback && fallback[key]) || key;
|
|
273
|
+
if (vars && typeof val === 'string') {
|
|
274
|
+
for (const k of Object.keys(vars)) {
|
|
275
|
+
val = val.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k]));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return val;
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
256
282
|
function createErrorBoundary() {
|
|
257
283
|
if (!React || typeof React.Component !== 'function') {
|
|
258
284
|
return function NoopBoundary(props) { return (props && props.children) || null; };
|
|
@@ -293,7 +319,9 @@ window.__ModuleLoader__.load({
|
|
|
293
319
|
const snap = ctx.locale.getSnapshot();
|
|
294
320
|
if (snap && snap.active && snap.active.startsWith('zh')) return zh;
|
|
295
321
|
}
|
|
296
|
-
} catch {
|
|
322
|
+
} catch (err) {
|
|
323
|
+
/* intentional fallback: default to english on snapshot error */
|
|
324
|
+
}
|
|
297
325
|
return en;
|
|
298
326
|
}
|
|
299
327
|
|
|
@@ -534,7 +562,7 @@ window.__ModuleLoader__.load({
|
|
|
534
562
|
style: {
|
|
535
563
|
position: 'absolute', top: 'calc(100% + 6px)', right: 0, width: 280,
|
|
536
564
|
background: 'var(--dsw-alias-bg-layer-3)', border: '1px solid var(--dsw-alias-border-l2)',
|
|
537
|
-
borderRadius: 10, boxShadow: '
|
|
565
|
+
borderRadius: 10, boxShadow: 'var(--dsw-alias-modal-shadow, var(--dsw-alias-shadow-large, 0 4px 16px var(--dsw-alias-border-l1)))', padding: '12px 14px',
|
|
538
566
|
zIndex: 1000, fontSize: 12, color: 'var(--dsw-alias-label-primary)', cursor: 'default', textAlign: 'left'
|
|
539
567
|
},
|
|
540
568
|
onClick: (e) => e.stopPropagation()
|
|
@@ -587,7 +615,7 @@ window.__ModuleLoader__.load({
|
|
|
587
615
|
const scopeRef = React.useRef(null);
|
|
588
616
|
if (!scopeRef.current && _ctx) {
|
|
589
617
|
try {
|
|
590
|
-
const s = (_ctx.get && _ctx.get('
|
|
618
|
+
const s = _ctx.settingsScope || (_ctx.get && _ctx.get('settingsScope'));
|
|
591
619
|
if (s && typeof s.bind === 'function') {
|
|
592
620
|
scopeRef.current = s.bind({ namespace: NS });
|
|
593
621
|
}
|
|
@@ -627,7 +655,9 @@ window.__ModuleLoader__.load({
|
|
|
627
655
|
unsub = scope.subscribe(() => {
|
|
628
656
|
if (!cancelled) loadSnap();
|
|
629
657
|
});
|
|
630
|
-
} catch (
|
|
658
|
+
} catch (_err) {
|
|
659
|
+
console.warn('[dsh-context-lens] scope subscribe failed', _err && _err.message || _err);
|
|
660
|
+
}
|
|
631
661
|
}
|
|
632
662
|
|
|
633
663
|
return () => {
|
|
@@ -636,6 +666,69 @@ window.__ModuleLoader__.load({
|
|
|
636
666
|
};
|
|
637
667
|
}, [scope]);
|
|
638
668
|
|
|
669
|
+
const [updateState, setUpdateState] = React.useState({
|
|
670
|
+
checking: false,
|
|
671
|
+
updating: false,
|
|
672
|
+
currentVersion: '0.1.18',
|
|
673
|
+
latestVersion: '',
|
|
674
|
+
updateAvailable: false,
|
|
675
|
+
canAutoUpdate: true,
|
|
676
|
+
notice: '',
|
|
677
|
+
error: ''
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
const checkUpdate = React.useCallback(async () => {
|
|
681
|
+
setUpdateState((s) => ({ ...s, checking: true, error: '' }));
|
|
682
|
+
try {
|
|
683
|
+
const res = await fetch('/api/dsh-context-lens/update');
|
|
684
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
685
|
+
const data = await res.json().catch(() => ({}));
|
|
686
|
+
setUpdateState((s) => ({
|
|
687
|
+
...s,
|
|
688
|
+
checking: false,
|
|
689
|
+
currentVersion: data.currentVersion || s.currentVersion,
|
|
690
|
+
latestVersion: data.latestVersion || '',
|
|
691
|
+
updateAvailable: !!data.updateAvailable,
|
|
692
|
+
canAutoUpdate: data.canAutoUpdate !== false,
|
|
693
|
+
}));
|
|
694
|
+
} catch (_) {
|
|
695
|
+
setUpdateState((s) => ({ ...s, checking: false }));
|
|
696
|
+
}
|
|
697
|
+
}, []);
|
|
698
|
+
|
|
699
|
+
React.useEffect(() => {
|
|
700
|
+
if (expanded) checkUpdate();
|
|
701
|
+
}, [expanded, checkUpdate]);
|
|
702
|
+
|
|
703
|
+
async function handleTriggerUpdate() {
|
|
704
|
+
if (updateState.updating) return;
|
|
705
|
+
setUpdateState((s) => ({ ...s, updating: true, error: '', notice: '' }));
|
|
706
|
+
try {
|
|
707
|
+
const res = await fetch('/api/dsh-context-lens/update', {
|
|
708
|
+
method: 'POST',
|
|
709
|
+
headers: { 'x-dsh-plugin-update': '1', 'x-dsh-context-lens-update': '1' }
|
|
710
|
+
});
|
|
711
|
+
const data = await res.json().catch(() => ({}));
|
|
712
|
+
if (!res.ok || data.ok === false || data.error) {
|
|
713
|
+
throw new Error(data.error || ('HTTP ' + res.status));
|
|
714
|
+
}
|
|
715
|
+
const newVer = data.updatedVersion || updateState.latestVersion || updateState.currentVersion;
|
|
716
|
+
setUpdateState((s) => ({
|
|
717
|
+
...s,
|
|
718
|
+
updating: false,
|
|
719
|
+
updateAvailable: false,
|
|
720
|
+
currentVersion: newVer,
|
|
721
|
+
notice: tt('update.done', { version: newVer }),
|
|
722
|
+
}));
|
|
723
|
+
} catch (e) {
|
|
724
|
+
setUpdateState((s) => ({
|
|
725
|
+
...s,
|
|
726
|
+
updating: false,
|
|
727
|
+
error: tt('update.failed', { error: e && e.message || String(e) }),
|
|
728
|
+
}));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
639
732
|
React.useEffect(() => {
|
|
640
733
|
if (!expanded) return;
|
|
641
734
|
fetch('/dsh-context-lens/status').then((r) => r.ok ? r.json() : null).then((j) => {
|
|
@@ -646,7 +739,8 @@ window.__ModuleLoader__.load({
|
|
|
646
739
|
}).catch(() => {});
|
|
647
740
|
}, [expanded, draft.autoCollapse]);
|
|
648
741
|
|
|
649
|
-
const
|
|
742
|
+
const L = React.useMemo(() => getActiveLocale(_ctx), [_ctx]);
|
|
743
|
+
const tt = React.useMemo(() => (t ? t : makeT(L, en)), [t, L]);
|
|
650
744
|
|
|
651
745
|
let ChevronIcon = null;
|
|
652
746
|
try {
|
|
@@ -655,9 +749,29 @@ window.__ModuleLoader__.load({
|
|
|
655
749
|
} catch (e) { ChevronIcon = null; }
|
|
656
750
|
|
|
657
751
|
const Chevron = ChevronIcon ? function ChevronNode(p) {
|
|
658
|
-
return React.createElement(ChevronIcon, { className: 'cl-chev' + (p.open ? ' cl-chev-open' : ''), style: { marginLeft: 'auto', color: 'var(--dsw-alias-label-tertiary)', transition: 'transform .16s', transform: p.open ? 'rotate(180deg)' : 'none' } });
|
|
752
|
+
return React.createElement(ChevronIcon, { className: 'cl-chev' + (p.open ? ' cl-chev-open' : ''), style: { marginLeft: 'auto', color: 'var(--dsw-alias-label-tertiary)', transition: 'transform .16s ease', transform: p.open ? 'rotate(180deg)' : 'none' } });
|
|
659
753
|
} : function Fallback(p) {
|
|
660
|
-
return React.createElement('
|
|
754
|
+
return React.createElement('svg', {
|
|
755
|
+
className: 'cl-chev' + (p.open ? ' cl-chev-open' : ''),
|
|
756
|
+
viewBox: '0 0 14 14',
|
|
757
|
+
width: 14,
|
|
758
|
+
height: 14,
|
|
759
|
+
style: {
|
|
760
|
+
marginLeft: 'auto',
|
|
761
|
+
color: 'var(--dsw-alias-label-tertiary)',
|
|
762
|
+
transform: p.open ? 'rotate(180deg)' : 'none',
|
|
763
|
+
transition: 'transform .16s ease',
|
|
764
|
+
display: 'inline-block',
|
|
765
|
+
flexShrink: 0
|
|
766
|
+
}
|
|
767
|
+
}, React.createElement('path', {
|
|
768
|
+
d: 'M3.5 5.25L7 8.75L10.5 5.25',
|
|
769
|
+
fill: 'none',
|
|
770
|
+
stroke: 'currentColor',
|
|
771
|
+
strokeWidth: '1.5',
|
|
772
|
+
strokeLinecap: 'round',
|
|
773
|
+
strokeLinejoin: 'round'
|
|
774
|
+
}));
|
|
661
775
|
};
|
|
662
776
|
|
|
663
777
|
async function onSave() {
|
|
@@ -711,6 +825,39 @@ window.__ModuleLoader__.load({
|
|
|
711
825
|
React.createElement(Chevron, { open: expanded })
|
|
712
826
|
),
|
|
713
827
|
expanded ? React.createElement('div', { className: 'cl-body', style: { borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 12 } },
|
|
828
|
+
React.createElement('div', {
|
|
829
|
+
style: {
|
|
830
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
831
|
+
padding: '6px 10px', marginBottom: 10, borderRadius: 8,
|
|
832
|
+
background: 'var(--dsw-alias-bg-layer-2)', border: '1px solid var(--dsw-alias-border-l2)', fontSize: 12
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
836
|
+
React.createElement('span', { style: { fontWeight: 500, color: 'var(--dsw-alias-label-primary)' } },
|
|
837
|
+
'v' + (updateState.currentVersion || '0.1.18')
|
|
838
|
+
),
|
|
839
|
+
updateState.checking
|
|
840
|
+
? React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 11 } }, tt('update.checking'))
|
|
841
|
+
: updateState.updateAvailable
|
|
842
|
+
? React.createElement('span', { className: 'cl-badge cl-badge-warn' },
|
|
843
|
+
tt('update.available', { latestVersion: updateState.latestVersion, currentVersion: updateState.currentVersion })
|
|
844
|
+
)
|
|
845
|
+
: React.createElement('span', { className: 'cl-badge cl-badge-ok' },
|
|
846
|
+
'✓ ' + tt('update.up_to_date')
|
|
847
|
+
)
|
|
848
|
+
),
|
|
849
|
+
updateState.updateAvailable
|
|
850
|
+
? React.createElement('button', {
|
|
851
|
+
type: 'button',
|
|
852
|
+
className: 'cl-btn cl-btn-primary',
|
|
853
|
+
disabled: updateState.updating,
|
|
854
|
+
onClick: handleTriggerUpdate,
|
|
855
|
+
style: { padding: '3px 8px', fontSize: 11 }
|
|
856
|
+
}, updateState.updating ? tt('update.updating') : tt('update.btn'))
|
|
857
|
+
: null
|
|
858
|
+
),
|
|
859
|
+
updateState.notice ? React.createElement('div', { className: 'cl-banner-ok', style: { marginBottom: 10, padding: '8px 12px', borderRadius: 8, background: 'color-mix(in srgb, var(--dsw-alias-state-success-primary, #10b981) 8%, transparent)', border: '1px solid var(--dsw-alias-state-success-primary, #10b981)', color: 'var(--dsw-alias-state-success-primary, #10b981)', fontSize: 12 } }, '✓ ' + updateState.notice) : null,
|
|
860
|
+
updateState.error ? React.createElement('div', { className: 'cl-banner-warn', style: { marginBottom: 10 } }, updateState.error) : null,
|
|
714
861
|
budgetWarn ? React.createElement('div', { className: 'cl-banner-warn', style: { marginBottom: 12 } }, '⚠ ' + tt('lowBudget')) : null,
|
|
715
862
|
status === 'loading' ? React.createElement('div', { style: { padding: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Loading…') :
|
|
716
863
|
status === 'unavailable' ? React.createElement('div', { style: { padding: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Settings unavailable — plugin not registered on host yet') :
|
|
@@ -815,9 +962,28 @@ window.__ModuleLoader__.load({
|
|
|
815
962
|
);
|
|
816
963
|
}
|
|
817
964
|
|
|
818
|
-
module.exports.inject = ['slots', 'locale'];
|
|
965
|
+
module.exports.inject = ['slots', 'locale', 'settingsScope'];
|
|
819
966
|
module.exports.apply = function apply(ctx) {
|
|
820
|
-
|
|
967
|
+
let hasEffect = false;
|
|
968
|
+
try {
|
|
969
|
+
hasEffect = typeof ctx.effect === 'function';
|
|
970
|
+
} catch (_) {
|
|
971
|
+
hasEffect = false;
|
|
972
|
+
}
|
|
973
|
+
if (hasEffect) {
|
|
974
|
+
ctx.effect(() => {
|
|
975
|
+
try {
|
|
976
|
+
const dispose = ctx.locale?.register ? ctx.locale.register(NS, { en, zh }) : undefined;
|
|
977
|
+
return () => {
|
|
978
|
+
if (typeof dispose === 'function') dispose();
|
|
979
|
+
};
|
|
980
|
+
} catch (e) {
|
|
981
|
+
console.warn('[dsh-context-lens] locale register failed', e && e.message || e);
|
|
982
|
+
}
|
|
983
|
+
}, 'dsh-context-lens: locale');
|
|
984
|
+
} else {
|
|
985
|
+
try { ctx.locale.register(NS, { en, zh }); } catch (e) { console.warn('[dsh-context-lens] locale register failed', e && e.message || e); }
|
|
986
|
+
}
|
|
821
987
|
if (!ctx.slots) return;
|
|
822
988
|
let registered = false;
|
|
823
989
|
const doRegister = () => {
|
|
@@ -890,17 +1056,28 @@ window.__ModuleLoader__.load({
|
|
|
890
1056
|
const ok = ctx.slots.inject("sidebar.right.pane.tab", registerPaneTab);
|
|
891
1057
|
if (!ok) console.warn("[dsh-context-lens] sidebar.right.pane.tab inject returned false");
|
|
892
1058
|
} catch (e) {
|
|
893
|
-
|
|
1059
|
+
console.warn("[dsh-context-lens] sidebar.right.pane.tab inject failed, falling back to direct register", e && e.message || e);
|
|
1060
|
+
try {
|
|
1061
|
+
registerPaneTab();
|
|
1062
|
+
} catch (e2) {
|
|
1063
|
+
console.warn("[dsh-context-lens] registerPaneTab direct register fallback failed", e2 && e2.message || e2);
|
|
1064
|
+
}
|
|
894
1065
|
}
|
|
895
1066
|
} else {
|
|
896
|
-
try {
|
|
1067
|
+
try {
|
|
1068
|
+
registerPaneTab();
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
console.warn("[dsh-context-lens] registerPaneTab direct register failed", err && err.message || err);
|
|
1071
|
+
}
|
|
897
1072
|
}
|
|
898
1073
|
}
|
|
899
1074
|
} catch (e) {
|
|
900
1075
|
console.warn("[dsh-context-lens] native sidebar registration failed", e && e.message || e);
|
|
901
1076
|
}
|
|
902
1077
|
});
|
|
903
|
-
} catch (
|
|
1078
|
+
} catch (err) {
|
|
1079
|
+
console.warn("[dsh-context-lens] native sidebar inject failed", err && err.message || err);
|
|
1080
|
+
}
|
|
904
1081
|
}
|
|
905
1082
|
|
|
906
1083
|
if (typeof ctx.inject === 'function') {
|
|
@@ -925,7 +1102,9 @@ window.__ModuleLoader__.load({
|
|
|
925
1102
|
console.warn('[dsh-context-lens] betterSidebar registerTab failed', e && e.message || e);
|
|
926
1103
|
}
|
|
927
1104
|
});
|
|
928
|
-
} catch (
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
console.warn('[dsh-context-lens] betterSidebar inject failed', err && err.message || err);
|
|
1107
|
+
}
|
|
929
1108
|
}
|
|
930
1109
|
|
|
931
1110
|
if (ctx.slots) {
|
|
@@ -945,11 +1124,19 @@ window.__ModuleLoader__.load({
|
|
|
945
1124
|
try {
|
|
946
1125
|
ctx.slots.inject('conversation.session.header.utilities', headerChipRegister);
|
|
947
1126
|
} catch (e) {
|
|
948
|
-
console.warn('[dsh-context-lens] utilities inject failed', e && e.message || e);
|
|
949
|
-
try {
|
|
1127
|
+
console.warn('[dsh-context-lens] utilities inject failed, falling back to direct register', e && e.message || e);
|
|
1128
|
+
try {
|
|
1129
|
+
headerChipRegister();
|
|
1130
|
+
} catch (e2) {
|
|
1131
|
+
console.warn('[dsh-context-lens] headerChipRegister fallback failed', e2 && e2.message || e2);
|
|
1132
|
+
}
|
|
950
1133
|
}
|
|
951
1134
|
} else {
|
|
952
|
-
try {
|
|
1135
|
+
try {
|
|
1136
|
+
headerChipRegister();
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
console.warn('[dsh-context-lens] headerChipRegister direct register failed', err && err.message || err);
|
|
1139
|
+
}
|
|
953
1140
|
}
|
|
954
1141
|
}
|
|
955
1142
|
};
|
package/lib/http.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export function writeJson(res, code, body) {
|
|
2
|
+
try {
|
|
3
|
+
res.writeHead(code, {
|
|
4
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
5
|
+
'Cache-Control': 'no-store',
|
|
6
|
+
});
|
|
7
|
+
res.end(JSON.stringify(body));
|
|
8
|
+
} catch {
|
|
9
|
+
/* socket already closed */
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readBody(req, maxBytes = 256 * 1024) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let size = 0;
|
|
17
|
+
req.on('data', (c) => {
|
|
18
|
+
size += c.length;
|
|
19
|
+
if (size > maxBytes) {
|
|
20
|
+
req.destroy();
|
|
21
|
+
reject(new Error('body too large'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
chunks.push(c);
|
|
25
|
+
});
|
|
26
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
27
|
+
req.on('error', reject);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function header(request, name) {
|
|
32
|
+
const value = request?.headers?.[name.toLowerCase()];
|
|
33
|
+
return Array.isArray(value) ? value[0] : value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isLoopbackAddress(value) {
|
|
37
|
+
const address = String(value || '').toLowerCase().replace(/^\[|\]$/g, '');
|
|
38
|
+
return address === 'localhost' || address === 'localhost.' || address === '::1'
|
|
39
|
+
|| address.startsWith('127.')
|
|
40
|
+
|| address.startsWith('::ffff:127.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Reject cross-site writes while allowing local, LAN, and reverse-proxy UIs. */
|
|
44
|
+
export function isTrustedRequest(request) {
|
|
45
|
+
const secFetchSite = header(request, 'sec-fetch-site');
|
|
46
|
+
if (secFetchSite === 'cross-site') {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const host = header(request, 'x-forwarded-host') || header(request, 'host');
|
|
51
|
+
const origin = header(request, 'origin');
|
|
52
|
+
if (origin) {
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(origin);
|
|
55
|
+
if (host && url.host.toLowerCase() === host.toLowerCase()) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (isLoopbackAddress(url.hostname) && isLoopbackAddress(request?.socket?.remoteAddress)) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const referer = header(request, 'referer');
|
|
68
|
+
if (referer) {
|
|
69
|
+
try {
|
|
70
|
+
const url = new URL(referer);
|
|
71
|
+
if (host && url.host.toLowerCase() === host.toLowerCase()) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (isLoopbackAddress(url.hostname) && isLoopbackAddress(request?.socket?.remoteAddress)) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Requests without origin/referer (e.g. curl, loopback tests, or internal calls)
|
|
84
|
+
if (isLoopbackAddress(request?.socket?.remoteAddress)) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (secFetchSite === 'same-origin' || secFetchSite === 'same-site') {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const UPDATE_HEADER = 'x-dsh-plugin-update';
|
|
96
|
+
export const ALT_UPDATE_HEADER = 'x-dsh-context-lens-update';
|
|
97
|
+
|
|
98
|
+
export function isTrustedUpdateRequest(request) {
|
|
99
|
+
const updHdr = header(request, UPDATE_HEADER) || header(request, ALT_UPDATE_HEADER);
|
|
100
|
+
if (updHdr !== '1') return false;
|
|
101
|
+
if (!isLoopbackAddress(request?.socket?.remoteAddress)) return false;
|
|
102
|
+
const site = header(request, 'sec-fetch-site');
|
|
103
|
+
if (site !== undefined && site !== 'same-origin') return false;
|
|
104
|
+
const origin = header(request, 'origin');
|
|
105
|
+
const host = header(request, 'host');
|
|
106
|
+
if (origin === undefined || host === undefined) return false;
|
|
107
|
+
try {
|
|
108
|
+
const url = new URL(origin);
|
|
109
|
+
return (url.protocol === 'http:' || url.protocol === 'https:')
|
|
110
|
+
&& isLoopbackAddress(url.hostname) && url.host === host;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|