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/CHANGELOG.md +17 -0
- package/README.md +15 -17
- package/lib/aggregation.js +14 -1
- package/lib/client.js +1 -1
- package/lib/http.js +26 -11
- package/lib/plugin.js +21 -2
- package/package.json +3 -3
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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.
|
|
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"
|