dsh-all-usage 1.1.15 → 1.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/http.js CHANGED
@@ -57,6 +57,26 @@ if (value === 'localhost') return true
57
57
  return isLoopbackAddress(value)
58
58
  }
59
59
 
60
+ // The DSH desktop shell serves the UI from its own scheme and forwards every
61
+ // request through Electron, which strips Origin (along with Cookie and
62
+ // Sec-Fetch-*) before the host sees it. A write therefore cannot present an
63
+ // Origin at all, and requiring one made every save from the desktop client fail
64
+ // with 403. What actually authorizes a write is the per-apply capability: it is
65
+ // unguessable and unreadable cross-origin (the read endpoints send no CORS
66
+ // headers), and a browser cannot attach it to a form POST. An Origin that is
67
+ // present must still be a trusted one, and the Host must still be loopback so a
68
+ // rebound DNS name cannot reach the API.
69
+ function isTrustedLocalOrigin(origin, hostUrl) {
70
+ if (origin === 'dsh-app://app' || origin.startsWith('dsh-app://')) return true
71
+ let originUrl
72
+ try {
73
+ originUrl = new URL(origin)
74
+ } catch (err) {
75
+ return false
76
+ }
77
+ return originUrl.protocol === 'http:' && originUrl.host === hostUrl.host
78
+ }
79
+
60
80
  function isTrustedLocalApiRequest(req, requireOrigin) {
61
81
  const remoteAddress = req && req.socket && req.socket.remoteAddress
62
82
  if (!isLoopbackAddress(remoteAddress)) return false
@@ -71,13 +91,8 @@ try {
71
91
  if (!isLoopbackHostname(hostUrl.hostname)) return false
72
92
  if (requestHeader(req, 'sec-fetch-site') === 'cross-site') return false
73
93
  const origin = requestHeader(req, 'origin')
74
- if (origin === undefined) return requireOrigin !== true
75
- try {
76
- const originUrl = new URL(origin)
77
- return originUrl.protocol === 'http:' && originUrl.host === hostUrl.host
78
- } catch (err) {
79
- return false
80
- }
94
+ if (origin === undefined || origin === '') return requireOrigin !== true
95
+ return isTrustedLocalOrigin(origin, hostUrl)
81
96
  }
82
97
 
83
98
  function hasWriteToken(req, expected) {
@@ -211,7 +226,7 @@ export function registerRoutes(host) {
211
226
  return
212
227
  }
213
228
  if (req.method !== 'POST') { res.statusCode = 405; res.end(); return }
214
- if (!isTrustedLocalApiRequest(req, true) || !hasWriteToken(req, state.requestToken)) { rejectRequest(res); return }
229
+ if (!isTrustedLocalApiRequest(req, false) || !hasWriteToken(req, state.requestToken)) { rejectRequest(res); return }
215
230
  const body = await readBody(req, 256 * 1024)
216
231
  if (body.tooLarge) { sendJson(res, 413, { ok: false, message: 'request-too-large' }); return }
217
232
  let args = null
@@ -232,7 +247,7 @@ export function registerRoutes(host) {
232
247
  path: '/api/all-usage/pricing/sync',
233
248
  handler: async (req, res) => {
234
249
  if (req.method !== 'POST') { res.statusCode = 405; res.end(); return }
235
- if (!isTrustedLocalApiRequest(req, true) || !hasWriteToken(req, state.requestToken)) { rejectRequest(res); return }
250
+ if (!isTrustedLocalApiRequest(req, false) || !hasWriteToken(req, state.requestToken)) { rejectRequest(res); return }
236
251
  // Backfill iterates the loaded ledger, so wait for it like the pricing
237
252
  // POST does; otherwise an early sync answers with an empty backfill.
238
253
  await Promise.all([state.pricingReady, state.ledgerReady])
@@ -245,7 +260,7 @@ export function registerRoutes(host) {
245
260
  path: '/api/all-usage/pricing/holidays',
246
261
  handler: async (req, res) => {
247
262
  if (req.method !== 'POST') { res.statusCode = 405; res.end(); return }
248
- if (!isTrustedLocalApiRequest(req, true) || !hasWriteToken(req, state.requestToken)) { rejectRequest(res); return }
263
+ if (!isTrustedLocalApiRequest(req, false) || !hasWriteToken(req, state.requestToken)) { rejectRequest(res); return }
249
264
  const body = await readBody(req, 8 * 1024)
250
265
  if (body.tooLarge) { sendJson(res, 413, { ok: false, message: 'request-too-large' }); return }
251
266
  let args = null
@@ -337,7 +352,7 @@ export function registerRoutes(host) {
337
352
  res.end()
338
353
  return
339
354
  }
340
- if (!isTrustedLocalApiRequest(req, true) || !hasWriteToken(req, state.requestToken)) { rejectRequest(res); return }
355
+ if (!isTrustedLocalApiRequest(req, false) || !hasWriteToken(req, state.requestToken)) { rejectRequest(res); return }
341
356
  const body = await readBody(req, 16 * 1024)
342
357
  if (body.tooLarge) { sendJson(res, 413, { ok: false, message: 'request-too-large' }); return }
343
358
  let args = null
package/lib/plugin.js CHANGED
@@ -1,4 +1,5 @@
1
- import { randomBytes } from 'node:crypto'
1
+ import { createHash, randomBytes } from 'node:crypto'
2
+ import { readFileSync } from 'node:fs'
2
3
  import { createAggregation } from './aggregation.js'
3
4
  import { createBalance } from './balance.js'
4
5
  import { registerRoutes } from './http.js'
@@ -8,6 +9,18 @@ import { createSessionSync } from './session-sync.js'
8
9
  import { createCostAccumulator, createEmptyPricingState } from './pricing.js'
9
10
 
10
11
  const name = 'dsh-all-usage'
12
+ // The page is served from this package, so a version mismatch between the
13
+ // running host and the page means DSH is still executing an older plugin (it
14
+ // only imports at startup). Publishing it lets the panel say so instead of
15
+ // leaving the user to guess why a new panel talks to an old payload.
16
+ const pluginVersion = (() => {
17
+ try {
18
+ const meta = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
19
+ return typeof meta.version === 'string' ? meta.version : ''
20
+ } catch (error) {
21
+ return ''
22
+ }
23
+ })()
11
24
  const inject = ['sessionQuery', 'workspaceRegistry', 'timer', 'sessionPersistence', 'storage', 'webServer']
12
25
 
13
26
  function apply(ctx) {
@@ -18,6 +31,7 @@ function apply(ctx) {
18
31
  webServer: ctx.get('webServer'),
19
32
  sessionPersistence: ctx.get('sessionPersistence'),
20
33
  }
34
+ const requestToken = randomBytes(32).toString('base64url')
21
35
  const state = {
22
36
  wsMeta: new Map(),
23
37
  // True once recorded usage from a vanished workspace was moved into the
@@ -52,8 +66,13 @@ function apply(ctx) {
52
66
  aliasWriteChain: Promise.resolve(),
53
67
  aliasesReady: Promise.resolve(),
54
68
  balanceCache: { fetchedAt: 0, payload: null },
55
- requestToken: randomBytes(32).toString('base64url'),
69
+ requestToken,
70
+ // The status poll must be able to notice that the capability rotated without
71
+ // being handed the capability itself: this id is derived from the token, so it
72
+ // changes exactly when the token does and reveals nothing about it.
73
+ capabilityId: createHash('sha256').update(requestToken).digest('base64url').slice(0, 16),
56
74
  instanceId: randomBytes(12).toString('base64url'),
75
+ pluginVersion,
57
76
  statsRevision: 0,
58
77
  dataRevision: 0,
59
78
  metadataRevision: 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-all-usage",
3
- "version": "1.1.15",
3
+ "version": "1.1.17",
4
4
  "description": "DeepSeek Harness usage dashboard with model, provider, workspace, cache, balance, and CSV insights",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "dsh": {
33
33
  "compatibility": {
34
- "runtime": ">=0.1.1-rc.1 <0.1.5-0 || >=0.1.5-rc.1 <0.1.6-0 || >=0.1.7-rc.2 <0.1.8-0",
35
- "verified": ["0.1.7-rc.2", "0.1.5-rc.2", "0.1.5-rc.1", "0.1.1-rc.2", "0.1.1-rc.1"]
34
+ "runtime": ">=0.1.1-rc.1 <0.1.5-0 || >=0.1.5-rc.1 <0.1.6-0 || >=0.1.7-rc.2 <0.1.8-0 || >=0.2.0-rc.2 <0.2.1-0",
35
+ "verified": ["0.2.0-rc.2", "0.1.7-rc.2", "0.1.5-rc.2", "0.1.5-rc.1", "0.1.1-rc.2", "0.1.1-rc.1"]
36
36
  },
37
37
  "bundle": {
38
38
  "patch": "./cordis.patch.yml"