@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 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 {
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
- if (host && url.host.toLowerCase() === host.toLowerCase()) {
134
- return true;
135
- }
136
- if (isLoopbackAddress(url.hostname) && isLoopbackAddress(request?.socket?.remoteAddress)) {
137
- return true;
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
- return false;
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.16",
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",