@goodandready/dsh-usage-guard 0.1.15 → 0.1.17
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 -37
- package/lib/telemetry.js +94 -5
- 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 {
|
|
@@ -156,8 +170,6 @@ export function apply(ctx, config) {
|
|
|
156
170
|
badFields: bad,
|
|
157
171
|
fixedTokenCount: fixedTokensCount,
|
|
158
172
|
clamped: clampedAny,
|
|
159
|
-
provider: event?.provider ?? event?.data?.provider ?? 'unknown',
|
|
160
|
-
sample: found.usage,
|
|
161
173
|
})
|
|
162
174
|
|
|
163
175
|
if (live && live.report !== false) {
|
package/lib/telemetry.js
CHANGED
|
@@ -11,7 +11,7 @@ class UsageGuardTelemetry {
|
|
|
11
11
|
this.recentIncidents = [];
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
recordIncident({ turn = 0, step = 0, badFields = [], fixedTokenCount = 0, clamped = false,
|
|
14
|
+
recordIncident({ turn = 0, step = 0, badFields = [], fixedTokenCount = 0, clamped = false, kind = 'malformed-usage', target = '' } = {}) {
|
|
15
15
|
this.rescuedEvents += 1;
|
|
16
16
|
this.fixedTokens += Number(fixedTokenCount) || 0;
|
|
17
17
|
if (clamped) {
|
|
@@ -26,8 +26,6 @@ class UsageGuardTelemetry {
|
|
|
26
26
|
target: String(target || (Array.isArray(badFields) ? badFields.join(',') : '')),
|
|
27
27
|
fields: Array.isArray(badFields) ? badFields : [],
|
|
28
28
|
clamped: Boolean(clamped),
|
|
29
|
-
provider: typeof provider === 'string' ? provider : 'unknown',
|
|
30
|
-
sample: sample && typeof sample === 'object' ? JSON.stringify(sample).slice(0, 300) : null,
|
|
31
29
|
};
|
|
32
30
|
|
|
33
31
|
this.lastIncident = incident;
|
|
@@ -85,8 +83,91 @@ export function getSnapshot() {
|
|
|
85
83
|
return telemetry.getSnapshot();
|
|
86
84
|
}
|
|
87
85
|
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
|
|
87
|
+
function header(request, name) {
|
|
88
|
+
const value = request?.headers?.[name.toLowerCase()];
|
|
89
|
+
return Array.isArray(value) ? value[0] : value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isLoopbackAddress(value) {
|
|
93
|
+
const address = String(value || '').toLowerCase().replace(/^[[\]]/g, '');
|
|
94
|
+
return address === 'localhost' || address === 'localhost.' || address === '::1'
|
|
95
|
+
|| address.startsWith('127.')
|
|
96
|
+
|| address.startsWith('::ffff:127.');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Reject cross-site and unauthenticated requests while allowing same-origin and local callers. */
|
|
100
|
+
export function isTrustedSettingsRequest(request, options = {}) {
|
|
101
|
+
if (!request || typeof request !== 'object' || !request.headers) return false;
|
|
102
|
+
|
|
103
|
+
const secFetchSite = header(request, 'sec-fetch-site');
|
|
104
|
+
// Unconditionally reject cross-site and same-site requests (#51)
|
|
105
|
+
if (secFetchSite === 'cross-site' || secFetchSite === 'same-site') {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const host = header(request, 'x-forwarded-host') || header(request, 'host');
|
|
110
|
+
const origin = header(request, 'origin');
|
|
111
|
+
if (origin) {
|
|
112
|
+
try {
|
|
113
|
+
const url = new URL(origin);
|
|
114
|
+
if (host && url.host.toLowerCase() === host.toLowerCase()) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const referer = header(request, 'referer');
|
|
124
|
+
if (referer) {
|
|
125
|
+
try {
|
|
126
|
+
const url = new URL(referer);
|
|
127
|
+
if (host && url.host.toLowerCase() === host.toLowerCase()) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check Bearer token or custom auth header
|
|
137
|
+
const expectedToken = options.expectedToken
|
|
138
|
+
|| (typeof process !== 'undefined' ? (process.env?.DSH_AUTH_TOKEN || process.env?.DSH_TOKEN) : null);
|
|
139
|
+
|
|
140
|
+
const auth = header(request, 'authorization');
|
|
141
|
+
if (typeof auth === 'string' && auth.startsWith('Bearer ')) {
|
|
142
|
+
const bearer = auth.slice(7).trim();
|
|
143
|
+
if (expectedToken && bearer && bearer === expectedToken) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const customAuth = header(request, 'x-dsh-auth');
|
|
149
|
+
if (expectedToken && customAuth && customAuth === expectedToken) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check cookie authentication
|
|
154
|
+
const cookie = header(request, 'cookie');
|
|
155
|
+
if (typeof cookie === 'string' && expectedToken) {
|
|
156
|
+
const match = cookie.match(/(?:^|;\s*)(?:token|dsh-auth|dsh_token)=([^;]+)/);
|
|
157
|
+
if (match && match[1] === expectedToken) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Loopback connection (curl, local process)
|
|
163
|
+
if (isLoopbackAddress(request?.socket?.remoteAddress)) {
|
|
164
|
+
if (secFetchSite && secFetchSite !== 'same-origin') {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return false;
|
|
90
171
|
}
|
|
91
172
|
|
|
92
173
|
export function registerTelemetryRoute(ctx, options = {}) {
|
|
@@ -100,6 +181,14 @@ export function registerTelemetryRoute(ctx, options = {}) {
|
|
|
100
181
|
response.end();
|
|
101
182
|
return;
|
|
102
183
|
}
|
|
184
|
+
if (!isTrustedSettingsRequest(request, options)) {
|
|
185
|
+
response.writeHead(403, {
|
|
186
|
+
'content-type': 'application/json; charset=utf-8',
|
|
187
|
+
'cache-control': 'no-store',
|
|
188
|
+
});
|
|
189
|
+
response.end(request.method === 'HEAD' ? undefined : JSON.stringify({ error: 'Forbidden: untrusted or cross-site request' }));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
103
192
|
const snapshot = telemetry.getSnapshot();
|
|
104
193
|
response.writeHead(200, {
|
|
105
194
|
'content-type': 'application/json; charset=utf-8',
|
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.17",
|
|
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",
|