@goodandready/dsh-cost-meter 0.7.7 → 0.7.8

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.
@@ -82,7 +82,7 @@
82
82
  - Не допускать утечек локальных путей и инфраструктурных данных в коде или документации.
83
83
 
84
84
  ## History & Decisions
85
- - 2026-09-05 (v0.7.7):
85
+ - 2026-09-05 (v0.7.8):
86
86
  - Hotfix: исправлен ReferenceError (t is not defined) в объекте словаря ru (lib/client.js) (#15).
87
87
  - Добавлен автоматический тест на чистое исполнение фабрики client loader (`test/public-route.test.mjs`).
88
88
  - 2026-09-05 (v0.7.4):
@@ -1,16 +1,10 @@
1
- # Technical Findings & Architecture Decisions (v0.7.7)
1
+ # Findings & Architecture Decisions (v0.7.8)
2
2
 
3
- ## 1. Zod Usage Rationale
4
- - `zod` is used in `lib/index.js` for `usageByModelProjection`:
5
- DeepSeek Harness core projection machinery requires `.parse(view)` method on projection schemas for runtime boundary contract validation. Schemastery does not provide `.parse()`, so `zod` is strictly required here and is not overengineering.
3
+ ## 1. Browser Request Race Conditions
4
+ In `CostMeter`, rapid model switches or toggling the popover calls `load(usedKeys)`. Since network latency varies, an older request might resolve after a newer one. By tracking an active request counter or cancellation token per effect invocation, we ensure only the most up-to-date state response mutates the component state.
6
5
 
7
- ## 2. Browser Reactivity & Settings
8
- - Reactivity in browser UI (`lib/client.js`):
9
- Using `React.useSyncExternalStore` connected to `ctx.settingsScope` allows instant updates of `currency` and `usdRate` in the header chip and popover without needing a page refresh or roundtrip state reload.
6
+ ## 2. IANA TimeZone Validation
7
+ `Intl.DateTimeFormat(undefined, { timeZone })` throws a `RangeError` if the timezone string is invalid. We can safely leverage this in `CostMeterCard` to reject typos (e.g. `Europe/Moskow` instead of `Europe/Moscow`) on the client before writing to `settingsScope`.
10
8
 
11
- ## 3. Slot Hygiene
12
- - Cleaned up slot injection in `lib/client.js`:
13
- Removed `setTimeout(..., 500)` fallback registering `settings.section`. The standard `settings.plugin.item` slot cleanly renders the settings card inside the Harness Settings view.
14
-
15
- ## 4. Style Isolation & Deduplication
16
- - Added `data-dsh-plugin="dsh-cost-meter"` attribute to injected `<style>` tags to adhere to DSH UI design guidelines and prevent duplicate stylesheets.
9
+ ## 3. Query Parameter Sanitization
10
+ The endpoint `GET /dsh-cost-meter/state?models=...` accepts comma-separated model keys. Adding a length bound (e.g. <= 128 chars) and trimming ensures no malformed payloads cause unexpected overhead.
@@ -1,15 +1,17 @@
1
- # Implementation Progress (v0.7.7)
1
+ # Implementation Progress (v0.7.8)
2
2
 
3
- - [x] Create Gitea Issue #17
4
- - [x] Create isolated worktree `refactor/stability-and-quality`
5
- - [x] Bump version to 0.7.7 in `package.json`
6
- - [x] Refactor `lib/client.js` for reactive settings, clean slot registration, and style tagging
7
- - [x] Verify syntax and run test suite (`npm test`, 9 passed)
8
- - [ ] Document design contract in `docs/design/DESIGN.md`
3
+ - [x] Create Gitea Issue #19
4
+ - [x] Create isolated worktree `stability-v078`
5
+ - [x] Bump version to 0.7.8 in `package.json`
6
+ - [x] Add monotonic request sequence guard in `lib/client.js` `CostMeter`
7
+ - [x] Add IANA time zone validation in `lib/client.js` `CostMeterCard`
8
+ - [x] Sanitize query parameters in `lib/index.js`
9
+ - [x] Add automated unit tests (`test/timezone-sanitization.test.mjs`, 11/11 passing)
10
+ - [x] Update design documentation in `docs/design/DESIGN.md`
9
11
  - [ ] Commit changes with conventional commits (`git-antigravity`)
10
12
  - [ ] Create Pull Request and merge into `main`
11
13
  - [ ] Test package on MiniPC isolated test server (`192.168.1.123:3082`)
12
14
  - [ ] Smoke-test package on production candidate (`192.168.1.111:3080`)
13
- - [ ] Publish v0.7.7 to npm and GitHub Release
14
- - [ ] Install v0.7.7 from npm on MiniAI and verify live health
15
- - [ ] Close Gitea Issue #17 and clean up worktree
15
+ - [ ] Publish v0.7.8 to npm and GitHub Release
16
+ - [ ] Install v0.7.8 from npm on MiniAI and verify live health
17
+ - [ ] Close Gitea Issue #19 and clean up worktree
@@ -1,25 +1,24 @@
1
- # Task Plan: Refactor dsh-cost-meter for stability, simplification & performance
1
+ # Task Plan: Stability, Race Condition Prevention & TimeZone Validation (v0.7.8)
2
2
 
3
- - **Issue**: Gitea #17
4
- - **Worktree**: `.worktrees/refactor-stability`
5
- - **Branch**: `refactor/stability-and-quality`
3
+ ## 1. Goal
4
+ Strengthen `@goodandready/dsh-cost-meter` runtime resilience:
5
+ - Prevent race condition response overwrites in browser `CostMeter` component.
6
+ - Validate IANA TimeZone inputs in `CostMeterCard` settings before saving.
7
+ - Sanitize `models` query parameter keys in `lib/index.js` route handler.
8
+ - Maintain strict backward compatibility, zero regression in automated tests.
6
9
 
7
- ## Phases
8
-
9
- - [ ] **Phase 1: Architecture & Overengineering Cleanup**
10
- - Remove `zod` dependency from `lib/index.js`, standardize on `@deepseek-ai/schemastery`.
11
- - Remove dynamic fragile `require('@deepseek-ai/dsh-client-ui-primitives')` from `lib/client.js`, standardize on clean native inline SVG chevron.
12
- - Remove race-prone `setTimeout(..., 500)` fallback slot registration in `lib/client.js`.
13
- - [ ] **Phase 2: Fault Tolerance & Safe Caching**
14
- - Wrap catalog filesystem operations (`mkdirSync`, `writeFileSync`, `readFileSync`) in safe `try/catch`.
15
- - Add safe error catching in `lib/client.js` for `/dsh-cost-meter/state` fetch to prevent console error spam on DSH reload.
16
- - [ ] **Phase 3: Performance & Settings Reactivity**
17
- - Implement in-memory catalog index Map in `lib/index.js` for O(1) model rate lookups.
18
- - Subscribe to `settingsScope` updates in `lib/client.js` to dynamically refresh currency and USD rate without reload.
19
- - [ ] **Phase 4: Verification, Tests & Documentation**
20
- - Run regression test suite `npm test` and add tests for schema validation and safe catalog failure.
21
- - Update `docs/design/DESIGN.md` and release notes for v0.7.7.
22
- - [ ] **Phase 5: Release Gate & Production Rollout**
23
- - Test on MiniPC test server (`192.168.1.123:3082`).
24
- - Merge PR, tag v0.7.7, publish to npm & GitHub release.
25
- - Deploy to production web profile on MiniAI (`192.168.1.111:3080`).
10
+ ## 2. Checklist
11
+ - [ ] Bump version to `0.7.8` in `package.json`
12
+ - [ ] Implement cancellation flag / abort protection in `lib/client.js` `CostMeter`
13
+ - [ ] Implement `isValidTimeZone` helper and validation check in `CostMeterCard`
14
+ - [ ] Implement key sanitization (length <= 128 chars, alphanumeric/dash/slash) in `lib/index.js`
15
+ - [ ] Add unit tests for timezone check and query sanitization
16
+ - [ ] Run test suite (`npm test`)
17
+ - [ ] Update `docs/design/DESIGN.md`
18
+ - [ ] Conventional Commit & Push
19
+ - [ ] Gitea PR & Merge to `main`
20
+ - [ ] Test on MiniPC test server (`192.168.1.123`)
21
+ - [ ] Production Smoke Verification on MiniAI (`192.168.1.111`)
22
+ - [ ] Publish Tag `v0.7.8`, GitHub Release & npm
23
+ - [ ] Update production profile from npm
24
+ - [ ] Close Gitea Issue #19 and clean up worktree
package/lib/client.js CHANGED
@@ -209,9 +209,14 @@ window.__ModuleLoader__.load({
209
209
  } catch (e) { broken.push('usdRate: ' + (e && e.message || String(e))) }
210
210
  }
211
211
 
212
- try {
213
- await scope.set('displayTimeZone', String(draft.displayTimeZone || 'Europe/Moscow').trim())
214
- } catch (e) { broken.push('displayTimeZone: ' + (e && e.message || String(e))) }
212
+ const tzVal = String(draft.displayTimeZone || 'Europe/Moscow').trim()
213
+ if (!isValidTimeZone(tzVal)) {
214
+ broken.push('displayTimeZone: invalid IANA time zone')
215
+ } else {
216
+ try {
217
+ await scope.set('displayTimeZone', tzVal)
218
+ } catch (e) { broken.push('displayTimeZone: ' + (e && e.message || String(e))) }
219
+ }
215
220
 
216
221
  setSaving(false)
217
222
  if (broken.length) {
@@ -385,6 +390,16 @@ window.__ModuleLoader__.load({
385
390
  return String(Math.floor(minute / 60)).padStart(2, '0') + ':' + String(minute % 60).padStart(2, '0')
386
391
  }
387
392
 
393
+ function isValidTimeZone(tz) {
394
+ if (!tz || typeof tz !== 'string') return false
395
+ try {
396
+ new Intl.DateTimeFormat(undefined, { timeZone: tz })
397
+ return true
398
+ } catch {
399
+ return false
400
+ }
401
+ }
402
+
388
403
  /**
389
404
  * Render a UTC minute-of-day in the display zone. Built from a real Date so
390
405
  * the zone's current offset (and any DST) comes from Intl, not arithmetic.
@@ -491,14 +506,18 @@ window.__ModuleLoader__.load({
491
506
  m.usage.cacheReadTokens || m.usage.cacheWriteTokens)
492
507
  const usedKeys = used.map((m) => m.provider + '/' + m.model).sort().join(',')
493
508
 
509
+ const reqSeq = React.useRef(0)
510
+
494
511
  const load = React.useCallback((keys) => {
512
+ const seq = ++reqSeq.current
495
513
  const query = keys ? '?models=' + encodeURIComponent(keys) : ''
496
514
  fetch(STATE_PATH + query, { headers: { accept: 'application/json' } })
497
515
  .then((r) => (r.ok ? r.json() : null))
498
- .then((data) => { if (data) setState(data) })
516
+ .then((data) => {
517
+ if (data && seq === reqSeq.current) setState(data)
518
+ })
499
519
  .catch(() => {})
500
520
  }, [])
501
-
502
521
  // Refetch when the set of billed models changes — a new model needs its
503
522
  // own tariff before its tokens can be priced.
504
523
  React.useEffect(() => { load(usedKeys) }, [load, usedKeys])
package/lib/index.js CHANGED
@@ -507,16 +507,18 @@ export function apply(ctx, config) {
507
507
  // session that switched models can price each part on its own tariff.
508
508
  const schedules = {}
509
509
  const asked = new URL(req.url, 'http://localhost').searchParams.get('models')
510
- for (const key of (asked ? asked.split(',') : []).slice(0, 24)) {
511
- const parsed = splitRoute(key.trim())
512
- if (!parsed) continue
513
- const resolved = resolveTariff(cfg, catalog.index, parsed)
514
- schedules[key.trim()] = {
515
- schedule: resolved.schedule,
516
- source: resolved.source,
517
- matchedId: resolved.matchedId,
518
- }
519
- }
510
+ for (const key of (asked ? asked.split(',') : []).slice(0, 24)) {
511
+ const trimmed = String(key || '').trim()
512
+ if (!trimmed || trimmed.length > 128) continue
513
+ const parsed = splitRoute(trimmed)
514
+ if (!parsed) continue
515
+ const resolved = resolveTariff(cfg, catalog.index, parsed)
516
+ schedules[trimmed] = {
517
+ schedule: resolved.schedule,
518
+ source: resolved.source,
519
+ matchedId: resolved.matchedId,
520
+ }
521
+ }
520
522
 
521
523
  res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
522
524
  res.end(JSON.stringify({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cost-meter",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
4
4
  "description": "DeepSeek Harness plugin: a session cost chip in the conversation header - live token usage by bucket, peak/off-peak tariff, and a countdown to the next tariff switch.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",