@goodandready/dsh-usage-guard 0.1.14 → 0.1.16

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/index.js CHANGED
@@ -13,7 +13,7 @@
13
13
  import z from '@deepseek-ai/schemastery'
14
14
 
15
15
  import { patchRegistry } from './patch.js'
16
- import { telemetry } from './telemetry.js'
16
+ import { telemetry, registerTelemetryRoute } from './telemetry.js'
17
17
  import { registerPluginUpdater } from './updater.js'
18
18
  import { complaint, damage, healed, usageOf } from './usage.js'
19
19
 
@@ -97,21 +97,8 @@ export function apply(ctx, config) {
97
97
  manifestUrl: new URL('../package.json', import.meta.url),
98
98
  }), 'dsh-usage-guard: plugin updater route')
99
99
 
100
- wctx.effect(() => wctx.webServer.register({
101
- kind: 'exact',
102
- path: '/api/dsh-usage-guard/telemetry',
103
- handler: async (req, res) => {
104
- if (req.method === 'GET' || req.method === 'HEAD') {
105
- res.writeHead(200, {
106
- 'content-type': 'application/json; charset=utf-8',
107
- 'cache-control': 'no-store',
108
- })
109
- res.end(req.method === 'HEAD' ? undefined : JSON.stringify(telemetry.getSnapshot()))
110
- return
111
- }
112
- res.writeHead(405, { allow: 'GET, HEAD' })
113
- res.end()
114
- },
100
+ wctx.effect(() => registerTelemetryRoute(wctx, {
101
+ endpoint: '/api/dsh-usage-guard/telemetry',
115
102
  }), 'dsh-usage-guard: telemetry route')
116
103
  } catch (err) {
117
104
  const msg = `[dsh-usage-guard] failed to register routes on webServer: ${err?.message || err}`
@@ -169,8 +156,6 @@ export function apply(ctx, config) {
169
156
  badFields: bad,
170
157
  fixedTokenCount: fixedTokensCount,
171
158
  clamped: clampedAny,
172
- provider: event?.provider ?? event?.data?.provider ?? 'unknown',
173
- sample: found.usage,
174
159
  })
175
160
 
176
161
  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;
@@ -89,6 +87,100 @@ export function resetTelemetry() {
89
87
  telemetry.reset();
90
88
  }
91
89
 
90
+ function header(request, name) {
91
+ const value = request?.headers?.[name.toLowerCase()];
92
+ return Array.isArray(value) ? value[0] : value;
93
+ }
94
+
95
+ function isLoopbackAddress(value) {
96
+ const address = String(value || '').toLowerCase().replace(/^[[\]]/g, '');
97
+ return address === 'localhost' || address === 'localhost.' || address === '::1'
98
+ || address.startsWith('127.')
99
+ || address.startsWith('::ffff:127.');
100
+ }
101
+
102
+ /** Reject cross-site and unauthenticated requests while allowing same-origin and local callers. */
103
+ export function isTrustedSettingsRequest(request, options = {}) {
104
+ if (!request || typeof request !== 'object' || !request.headers) return false;
105
+
106
+ const secFetchSite = header(request, 'sec-fetch-site');
107
+ // Unconditionally reject cross-site requests
108
+ if (secFetchSite === 'cross-site') {
109
+ return false;
110
+ }
111
+
112
+ const host = header(request, 'x-forwarded-host') || header(request, 'host');
113
+ const origin = header(request, 'origin');
114
+ if (origin) {
115
+ try {
116
+ const url = new URL(origin);
117
+ if (host && url.host.toLowerCase() === host.toLowerCase()) {
118
+ return true;
119
+ }
120
+ if (isLoopbackAddress(url.hostname) && isLoopbackAddress(request?.socket?.remoteAddress)) {
121
+ return true;
122
+ }
123
+ return false;
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ const referer = header(request, 'referer');
130
+ if (referer) {
131
+ try {
132
+ 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;
138
+ }
139
+ return false;
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ // Check Bearer token or custom auth header
146
+ const expectedToken = options.expectedToken
147
+ || (typeof process !== 'undefined' ? (process.env?.DSH_AUTH_TOKEN || process.env?.DSH_TOKEN) : null);
148
+
149
+ const auth = header(request, 'authorization');
150
+ if (typeof auth === 'string' && auth.startsWith('Bearer ')) {
151
+ const bearer = auth.slice(7).trim();
152
+ if (expectedToken && bearer && bearer === expectedToken) {
153
+ return true;
154
+ }
155
+ }
156
+
157
+ const customAuth = header(request, 'x-dsh-auth');
158
+ if (expectedToken && customAuth && customAuth === expectedToken) {
159
+ return true;
160
+ }
161
+
162
+ // Check cookie authentication
163
+ const cookie = header(request, 'cookie');
164
+ if (typeof cookie === 'string' && expectedToken) {
165
+ const match = cookie.match(/(?:^|;\s*)(?:token|dsh-auth|dsh_token)=([^;]+)/);
166
+ if (match && match[1] === expectedToken) {
167
+ return true;
168
+ }
169
+ }
170
+
171
+ // Same-origin or same-site fetch
172
+ if (secFetchSite === 'same-origin' || secFetchSite === 'same-site') {
173
+ return true;
174
+ }
175
+
176
+ // Loopback connection (curl, local process)
177
+ if (isLoopbackAddress(request?.socket?.remoteAddress)) {
178
+ return true;
179
+ }
180
+
181
+ return false;
182
+ }
183
+
92
184
  export function registerTelemetryRoute(ctx, options = {}) {
93
185
  const endpoint = options.endpoint || '/api/dsh-usage-guard/telemetry';
94
186
  return ctx.webServer.register({
@@ -100,6 +192,14 @@ export function registerTelemetryRoute(ctx, options = {}) {
100
192
  response.end();
101
193
  return;
102
194
  }
195
+ if (!isTrustedSettingsRequest(request, options)) {
196
+ response.writeHead(403, {
197
+ 'content-type': 'application/json; charset=utf-8',
198
+ 'cache-control': 'no-store',
199
+ });
200
+ response.end(request.method === 'HEAD' ? undefined : JSON.stringify({ error: 'Forbidden: untrusted or cross-site request' }));
201
+ return;
202
+ }
103
203
  const snapshot = telemetry.getSnapshot();
104
204
  response.writeHead(200, {
105
205
  'content-type': 'application/json; charset=utf-8',
package/lib/updater.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // Implements canonical DSH plugin-updater specification.
3
3
 
4
4
  import { spawn } from 'node:child_process';
5
- import { existsSync, readFileSync } from 'node:fs';
5
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
6
6
  import { readFile } from 'node:fs/promises';
7
7
  import { homedir } from 'node:os';
8
8
  import { basename, dirname, isAbsolute, resolve } from 'node:path';
@@ -55,11 +55,16 @@ function profileNameFromArgv(argv) {
55
55
  return argv[2] === 'web' ? 'web' : undefined;
56
56
  }
57
57
 
58
- function findDshCliEntry() {
59
- const value = process.argv[1];
58
+ export function findDshCliEntry(argv1 = process.argv[1]) {
59
+ const value = argv1;
60
60
  if (value === undefined || value === '') return undefined;
61
- const entry = value.startsWith('file:') ? fileURLToPath(value) : resolve(process.cwd(), value);
61
+ let entry = value.startsWith('file:') ? fileURLToPath(value) : resolve(process.cwd(), value);
62
62
  if (!existsSync(entry)) return undefined;
63
+ try {
64
+ entry = realpathSync(entry);
65
+ } catch {
66
+ // Ignore symlink resolution errors and continue with unresolved path
67
+ }
63
68
  for (let directory = dirname(entry); ; directory = dirname(directory)) {
64
69
  const manifestPath = resolve(directory, 'package.json');
65
70
  if (existsSync(manifestPath)) {
package/lib/usage.js CHANGED
@@ -102,10 +102,32 @@ export function usageOf(event) {
102
102
  * and the harness automatically defaults them to zero. We only flag values that actually
103
103
  * break arithmetic operations — missing required counters, non-numbers, non-integers, or negatives.
104
104
  */
105
+ const clampedMap = new WeakMap()
106
+
107
+ export function clampedDetails(usageOrBad, options = {}) {
108
+ if (Array.isArray(usageOrBad)) {
109
+ return clampedMap.get(usageOrBad) ?? null
110
+ }
111
+ const usage = usageOrBad
112
+ if (!usage || typeof usage !== 'object') return null
113
+ const maxStepTokens = typeof options === 'number' ? options : (Number(options?.maxStepTokens) > 0 ? Number(options.maxStepTokens) : 0)
114
+ if (maxStepTokens <= 0) return null
115
+ let clamped = null
116
+ for (const field of FIELDS) {
117
+ const value = usage[field]
118
+ if (typeof value === 'number' && Number.isFinite(value) && value > maxStepTokens) {
119
+ if (!clamped) clamped = {}
120
+ clamped[field] = { from: value, to: maxStepTokens }
121
+ }
122
+ }
123
+ return clamped
124
+ }
125
+
105
126
  export function damage(usage, options = {}) {
106
127
  if (!usage || typeof usage !== 'object') return FIELDS.slice(0, 2)
107
128
  const bad = []
108
129
  const maxStepTokens = typeof options === 'number' ? options : (Number(options?.maxStepTokens) > 0 ? Number(options.maxStepTokens) : 0)
130
+ let clamped = null
109
131
 
110
132
  for (const field of FIELDS) {
111
133
  const value = usage[field]
@@ -115,10 +137,13 @@ export function damage(usage, options = {}) {
115
137
  bad.push(field)
116
138
  } else if (maxStepTokens > 0 && value > maxStepTokens) {
117
139
  bad.push(field)
118
- bad.clamped = bad.clamped || {}
119
- bad.clamped[field] = { from: value, to: maxStepTokens }
140
+ if (!clamped) clamped = {}
141
+ clamped[field] = { from: value, to: maxStepTokens }
120
142
  }
121
143
  }
144
+ if (clamped) {
145
+ clampedMap.set(bad, clamped)
146
+ }
122
147
  return bad
123
148
  }
124
149
 
@@ -129,30 +154,30 @@ export function damage(usage, options = {}) {
129
154
  export function borrowed(usage, field) {
130
155
  if (!usage || typeof usage !== 'object') return undefined
131
156
 
132
- // 1. Search known alias names
157
+ // 1. Check the canonical field itself first if passed as a string or float (e.g. "123" or 15.4)
158
+ const selfNum = coerceNumber(usage[field])
159
+ if (selfNum !== undefined) return selfNum
160
+
161
+ // 2. Search known alias names
133
162
  for (const alias of ALIASES[field] ?? []) {
134
163
  const num = coerceNumber(usage[alias])
135
164
  if (num !== undefined) return num
136
165
  }
137
166
 
138
- // 2. Search nested cache details (OpenAI-style: prompt_tokens_details.cached_tokens / cachedTokens)
167
+ // 3. Search nested cache details (OpenAI-style: prompt_tokens_details.cached_tokens / cachedTokens)
139
168
  if (field === 'cacheReadTokens' && usage.prompt_tokens_details && typeof usage.prompt_tokens_details === 'object') {
140
169
  const details = usage.prompt_tokens_details
141
170
  const num = coerceNumber(details.cached_tokens ?? details.cachedTokens ?? details.cache_read_tokens)
142
171
  if (num !== undefined) return num
143
172
  }
144
173
 
145
- // 3. Search nested cache creation details (cache_creation_input_tokens)
174
+ // 4. Search nested cache creation details (cache_creation_input_tokens)
146
175
  if (field === 'cacheWriteTokens' && usage.prompt_tokens_details && typeof usage.prompt_tokens_details === 'object') {
147
176
  const details = usage.prompt_tokens_details
148
177
  const num = coerceNumber(details.cache_creation_tokens ?? details.cacheCreationTokens)
149
178
  if (num !== undefined) return num
150
179
  }
151
180
 
152
- // 4. Check the field itself if passed as a string or float (e.g. "123" or 15.4)
153
- const selfNum = coerceNumber(usage[field])
154
- if (selfNum !== undefined) return selfNum
155
-
156
181
  return undefined
157
182
  }
158
183
 
@@ -223,12 +248,13 @@ export function complaint(found, bad, options = {}) {
223
248
  const taken = Array.isArray(bad)
224
249
  ? bad
225
250
  .map((field) => {
226
- if (bad?.clamped?.[field]) {
227
- return `${field} clamped abnormally large (${bad.clamped[field].from} -> ${bad.clamped[field].to})`
251
+ const clamped = options?.clamped ?? clampedMap.get(bad) ?? clampedDetails(usage, options)
252
+ if (clamped?.[field]) {
253
+ return `${field} clamped abnormally large (${clamped[field].from} -> ${clamped[field].to})`
228
254
  }
229
255
  const val = usage?.[field]
230
256
  if (maxStepTokens > 0 && typeof val === 'number' && val > maxStepTokens) {
231
- return `${field} clamped to ${maxStepTokens}`
257
+ return `${field} clamped abnormally large (${val} -> ${maxStepTokens})`
232
258
  }
233
259
  return usage && borrowed(usage, field) !== undefined ? `${field} recovered via alias` : `${field} zeroed`
234
260
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-usage-guard",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
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",