@goodandready/dsh-usage-guard 0.1.16 → 0.1.18
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/lib/client.js +14 -4
- package/lib/index.js +49 -35
- package/lib/telemetry.js +14 -20
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -52,6 +52,7 @@ window.__ModuleLoader__.load({
|
|
|
52
52
|
updater_up_to_date: 'Plugin is up to date',
|
|
53
53
|
updater_failed: 'Update check failed',
|
|
54
54
|
updater_success: 'Update successful. Please reload page.',
|
|
55
|
+
request_timeout: 'Request timed out (15s)',
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
const zh = {
|
|
@@ -97,6 +98,7 @@ window.__ModuleLoader__.load({
|
|
|
97
98
|
updater_up_to_date: '已是最新版本',
|
|
98
99
|
updater_failed: '检查更新失败',
|
|
99
100
|
updater_success: '更新成功,请刷新页面。',
|
|
101
|
+
request_timeout: '请求超时(15秒)',
|
|
100
102
|
};
|
|
101
103
|
|
|
102
104
|
function getActiveLocale(c) {
|
|
@@ -248,16 +250,18 @@ window.__ModuleLoader__.load({
|
|
|
248
250
|
const res = await fetch('/api/dsh-usage-guard/telemetry', {
|
|
249
251
|
headers: { accept: 'application/json' },
|
|
250
252
|
cache: 'no-store',
|
|
253
|
+
signal: AbortSignal.timeout(15000),
|
|
251
254
|
});
|
|
252
255
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
253
256
|
const data = await res.json();
|
|
254
257
|
setTelemetry(data);
|
|
255
258
|
} catch (e) {
|
|
256
|
-
|
|
259
|
+
const isTimeout = e?.name === 'TimeoutError' || e?.name === 'AbortError';
|
|
260
|
+
setErr(isTimeout ? t('request_timeout') : (e?.message || String(e)));
|
|
257
261
|
} finally {
|
|
258
262
|
setLoading(false);
|
|
259
263
|
}
|
|
260
|
-
}, []);
|
|
264
|
+
}, [t]);
|
|
261
265
|
|
|
262
266
|
React.useEffect(() => {
|
|
263
267
|
fetchTelemetry();
|
|
@@ -341,12 +345,15 @@ window.__ModuleLoader__.load({
|
|
|
341
345
|
const res = await fetch('/api/dsh-usage-guard/update', {
|
|
342
346
|
headers: { accept: 'application/json' },
|
|
343
347
|
cache: 'no-store',
|
|
348
|
+
signal: AbortSignal.timeout(15000),
|
|
344
349
|
});
|
|
345
350
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
346
351
|
const data = await res.json();
|
|
347
352
|
setStatus(data);
|
|
348
353
|
} catch (e) {
|
|
349
|
-
|
|
354
|
+
const isTimeout = e?.name === 'TimeoutError' || e?.name === 'AbortError';
|
|
355
|
+
const msg = isTimeout ? t('request_timeout') : (e?.message || String(e));
|
|
356
|
+
setFeedback({ text: `${t('updater_failed')}: ${msg}`, ok: false });
|
|
350
357
|
} finally {
|
|
351
358
|
setLoading(false);
|
|
352
359
|
}
|
|
@@ -362,13 +369,16 @@ window.__ModuleLoader__.load({
|
|
|
362
369
|
'x-dsh-plugin-update': '1',
|
|
363
370
|
'content-type': 'application/json',
|
|
364
371
|
},
|
|
372
|
+
signal: AbortSignal.timeout(15000),
|
|
365
373
|
});
|
|
366
374
|
const data = await res.json();
|
|
367
375
|
if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
|
|
368
376
|
setStatus(data);
|
|
369
377
|
setFeedback({ text: t('updater_success'), ok: true });
|
|
370
378
|
} catch (e) {
|
|
371
|
-
|
|
379
|
+
const isTimeout = e?.name === 'TimeoutError' || e?.name === 'AbortError';
|
|
380
|
+
const msg = isTimeout ? t('request_timeout') : (e?.message || String(e));
|
|
381
|
+
setFeedback({ text: msg, ok: false });
|
|
372
382
|
} finally {
|
|
373
383
|
setUpdating(false);
|
|
374
384
|
}
|
package/lib/index.js
CHANGED
|
@@ -25,69 +25,83 @@ export const Config = z.object({
|
|
|
25
25
|
repair: z
|
|
26
26
|
.boolean()
|
|
27
27
|
.default(true)
|
|
28
|
+
.volatile()
|
|
28
29
|
.description('Replace a missing or non-numeric token counter with zero before the harness adds it up. '
|
|
29
30
|
+ 'Off means reporting only: the counters reach the fold as they came, and a broken sample keeps the '
|
|
30
31
|
+ 'session history unreadable.'),
|
|
31
32
|
report: z
|
|
32
33
|
.boolean()
|
|
33
34
|
.default(true)
|
|
35
|
+
.volatile()
|
|
34
36
|
.description('Log a line naming the turn, the step and the raw sample whenever a damaged one arrives. '
|
|
35
37
|
+ 'Each turn, step and field set is reported once, not once per replay.'),
|
|
36
38
|
maxStepTokens: z
|
|
37
39
|
.natural()
|
|
38
40
|
.default(0)
|
|
41
|
+
.volatile()
|
|
39
42
|
.description('Maximum allowed token count for a single turn step. Spikes exceeding this ceiling are clamped to prevent integer overflow and runaway stats. Set to 0 to disable.'),
|
|
40
43
|
})
|
|
41
44
|
|
|
45
|
+
export function plainConfig(val) {
|
|
46
|
+
if (!val || typeof val !== 'object') return val
|
|
47
|
+
if (typeof val.get === 'function') return plainConfig(val.get())
|
|
48
|
+
const out = Array.isArray(val) ? [] : {}
|
|
49
|
+
for (const [k, v] of Object.entries(val)) out[k] = plainConfig(v)
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
export function apply(ctx, config) {
|
|
43
54
|
let live
|
|
44
55
|
try {
|
|
45
|
-
|
|
56
|
+
const plain = plainConfig(config ?? {})
|
|
57
|
+
live = plainConfig(Config(plain)) ?? plain
|
|
46
58
|
} catch (_) {
|
|
47
59
|
live = Config({}) ?? { repair: true, report: true, maxStepTokens: 0 }
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
|
|
62
|
+
let settingsSvc = null
|
|
63
|
+
const readLive = () => {
|
|
51
64
|
try {
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
if (typeof sctx.logger?.warn === 'function') sctx.logger.warn(msg)
|
|
56
|
-
else if (typeof ctx.logger?.warn === 'function') ctx.logger.warn(msg)
|
|
57
|
-
return
|
|
65
|
+
const row = settingsSvc?.describe?.()?.find?.((r) => r?.ns === NS)
|
|
66
|
+
if (row?.value) {
|
|
67
|
+
return plainConfig(Config(plainConfig(row.value)))
|
|
58
68
|
}
|
|
59
|
-
|
|
60
|
-
if (typeof
|
|
61
|
-
|
|
62
|
-
const fresh = scope.get()
|
|
63
|
-
if (fresh && typeof fresh === 'object') live = fresh
|
|
64
|
-
} catch (readErr) {
|
|
65
|
-
const msg = `[dsh-usage-guard] failed to read current settings: ${readErr?.message || readErr}; running with initial config`
|
|
66
|
-
if (typeof sctx.logger?.warn === 'function') sctx.logger.warn(msg)
|
|
67
|
-
else if (typeof ctx.logger?.warn === 'function') ctx.logger.warn(msg)
|
|
68
|
-
}
|
|
69
|
-
} else {
|
|
70
|
-
const msg = `[dsh-usage-guard] settings scope has no get() method; running with default config`
|
|
71
|
-
if (typeof sctx.logger?.warn === 'function') sctx.logger.warn(msg)
|
|
72
|
-
else if (typeof ctx.logger?.warn === 'function') ctx.logger.warn(msg)
|
|
69
|
+
} catch (describeErr) {
|
|
70
|
+
if (typeof ctx.logger?.debug === 'function') {
|
|
71
|
+
ctx.logger.debug('[dsh-usage-guard] failed to read settings via describe():', describeErr)
|
|
73
72
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const msg = `[dsh-usage-guard] settings scope has no watch() method; running with default config without live update subscription`
|
|
81
|
-
if (typeof sctx.logger?.warn === 'function') sctx.logger.warn(msg)
|
|
82
|
-
else if (typeof ctx.logger?.warn === 'function') ctx.logger.warn(msg)
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
return plainConfig(Config(plainConfig(config)))
|
|
76
|
+
} catch (configErr) {
|
|
77
|
+
if (typeof ctx.logger?.debug === 'function') {
|
|
78
|
+
ctx.logger.debug('[dsh-usage-guard] failed to derive config from base:', configErr)
|
|
83
79
|
}
|
|
84
|
-
|
|
85
|
-
const msg = `[dsh-usage-guard] failed to register settings subscription: ${err?.message || err}; running with default config`
|
|
86
|
-
if (typeof sctx.logger?.warn === 'function') sctx.logger.warn(msg)
|
|
87
|
-
else if (typeof ctx.logger?.warn === 'function') ctx.logger.warn(msg)
|
|
80
|
+
return live
|
|
88
81
|
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
ctx.inject(['settings'], (sctx) => {
|
|
85
|
+
settingsSvc = sctx.settings ?? (typeof sctx.get === 'function' ? sctx.get('settings') : null) ?? null
|
|
86
|
+
const fresh = readLive()
|
|
87
|
+
if (fresh) live = fresh
|
|
89
88
|
})
|
|
90
89
|
|
|
90
|
+
if (typeof ctx.effect === 'function') {
|
|
91
|
+
ctx.effect(() => {
|
|
92
|
+
const onVolatileUpdate = () => {
|
|
93
|
+
const fresh = readLive()
|
|
94
|
+
if (fresh) live = fresh
|
|
95
|
+
}
|
|
96
|
+
ctx.on?.('loader/volatile-update', onVolatileUpdate)
|
|
97
|
+
ctx.on?.('settings/document-updated', onVolatileUpdate)
|
|
98
|
+
return () => {
|
|
99
|
+
ctx.off?.('loader/volatile-update', onVolatileUpdate)
|
|
100
|
+
ctx.off?.('settings/document-updated', onVolatileUpdate)
|
|
101
|
+
}
|
|
102
|
+
}, 'dsh-usage-guard: volatile-update')
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
// Register one-click updater and telemetry endpoints if webServer is available
|
|
92
106
|
ctx.inject(['webServer'], (wctx) => {
|
|
93
107
|
try {
|
package/lib/telemetry.js
CHANGED
|
@@ -83,9 +83,6 @@ export function getSnapshot() {
|
|
|
83
83
|
return telemetry.getSnapshot();
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
export function resetTelemetry() {
|
|
87
|
-
telemetry.reset();
|
|
88
|
-
}
|
|
89
86
|
|
|
90
87
|
function header(request, name) {
|
|
91
88
|
const value = request?.headers?.[name.toLowerCase()];
|
|
@@ -104,8 +101,8 @@ export function isTrustedSettingsRequest(request, options = {}) {
|
|
|
104
101
|
if (!request || typeof request !== 'object' || !request.headers) return false;
|
|
105
102
|
|
|
106
103
|
const secFetchSite = header(request, 'sec-fetch-site');
|
|
107
|
-
// Unconditionally reject cross-site requests
|
|
108
|
-
if (secFetchSite === 'cross-site') {
|
|
104
|
+
// Unconditionally reject cross-site and same-site requests (#51)
|
|
105
|
+
if (secFetchSite === 'cross-site' || secFetchSite === 'same-site') {
|
|
109
106
|
return false;
|
|
110
107
|
}
|
|
111
108
|
|
|
@@ -117,9 +114,6 @@ export function isTrustedSettingsRequest(request, options = {}) {
|
|
|
117
114
|
if (host && url.host.toLowerCase() === host.toLowerCase()) {
|
|
118
115
|
return true;
|
|
119
116
|
}
|
|
120
|
-
if (isLoopbackAddress(url.hostname) && isLoopbackAddress(request?.socket?.remoteAddress)) {
|
|
121
|
-
return true;
|
|
122
|
-
}
|
|
123
117
|
return false;
|
|
124
118
|
} catch {
|
|
125
119
|
return false;
|
|
@@ -130,15 +124,17 @@ export function isTrustedSettingsRequest(request, options = {}) {
|
|
|
130
124
|
if (referer) {
|
|
131
125
|
try {
|
|
132
126
|
const url = new URL(referer);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
127
|
+
// Apply strict host matching only to standard web referers (http/https).
|
|
128
|
+
// Non-http schemes (e.g. dsh-app:// in DSH Desktop Electron forwarder)
|
|
129
|
+
// fall through to loopback and token verification (#55).
|
|
130
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
131
|
+
if (host && url.host.toLowerCase() === host.toLowerCase()) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
138
135
|
}
|
|
139
|
-
return false;
|
|
140
136
|
} catch {
|
|
141
|
-
|
|
137
|
+
// Non-standard or unparseable referers fall through to auth/loopback checks
|
|
142
138
|
}
|
|
143
139
|
}
|
|
144
140
|
|
|
@@ -168,13 +164,11 @@ export function isTrustedSettingsRequest(request, options = {}) {
|
|
|
168
164
|
}
|
|
169
165
|
}
|
|
170
166
|
|
|
171
|
-
// Same-origin or same-site fetch
|
|
172
|
-
if (secFetchSite === 'same-origin' || secFetchSite === 'same-site') {
|
|
173
|
-
return true;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
167
|
// Loopback connection (curl, local process)
|
|
177
168
|
if (isLoopbackAddress(request?.socket?.remoteAddress)) {
|
|
169
|
+
if (secFetchSite && secFetchSite !== 'same-origin') {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
178
172
|
return true;
|
|
179
173
|
}
|
|
180
174
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-usage-guard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Keeps a malformed token-usage sample from taking a whole session history down with it: a missing or non-numeric counter becomes zero before the harness adds it up, so the fold never yields NaN and already-broken sessions open again.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|