@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 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
- setErr(e?.message || String(e));
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
- setFeedback({ text: `${t('updater_failed')}: ${e?.message || String(e)}`, ok: false });
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
- setFeedback({ text: e?.message || String(e), ok: false });
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
- live = Config(structuredClone(config ?? {})) ?? config
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
- ctx.inject(['settings'], (sctx) => {
62
+ let settingsSvc = null
63
+ const readLive = () => {
51
64
  try {
52
- const scope = sctx.settings?.register?.(NS, Config, { base: config })
53
- if (!scope) {
54
- const msg = `[dsh-usage-guard] settings service register() unavailable; running with default config`
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 scope.get === 'function') {
61
- try {
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
- if (typeof scope.watch === 'function') {
76
- sctx.effect(() => scope.watch((next) => {
77
- if (next && typeof next === 'object') live = next
78
- }))
79
- } else {
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
- } catch (err) {
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, provider = 'unknown', sample = null, kind = 'malformed-usage', target = '' } = {}) {
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
- export function resetTelemetry() {
89
- telemetry.reset();
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.15",
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",