@goodandready/dsh-subscriptions 0.6.71 → 0.6.82

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.
Files changed (50) hide show
  1. package/README.md +13 -0
  2. package/README.ru.md +13 -0
  3. package/README.zh.md +13 -0
  4. package/lib/accounts-view.js +101 -5
  5. package/lib/accounts.js +150 -11
  6. package/lib/adapter.js +3 -0
  7. package/lib/alerts.js +35 -0
  8. package/lib/analytics.js +31 -2
  9. package/lib/billing.js +99 -0
  10. package/lib/blob.js +18 -0
  11. package/lib/capabilities.js +208 -0
  12. package/lib/client.js +944 -828
  13. package/lib/config-schema.js +46 -4
  14. package/lib/diagnostic-wizard.js +2 -5
  15. package/lib/diagnostics.js +2 -2
  16. package/lib/doctor.js +163 -0
  17. package/lib/error-buffer.js +2 -2
  18. package/lib/forecast.js +56 -0
  19. package/lib/headless-auth.js +379 -0
  20. package/lib/history.js +36 -0
  21. package/lib/http.js +26 -4
  22. package/lib/index.js +98 -11
  23. package/lib/instructions.js +138 -0
  24. package/lib/jwt.js +84 -17
  25. package/lib/loopback.js +3 -2
  26. package/lib/mask.js +16 -0
  27. package/lib/oauth-pending.js +12 -0
  28. package/lib/pricing.js +89 -0
  29. package/lib/refs.js +2 -0
  30. package/lib/reset-clock.js +55 -0
  31. package/lib/rotate.js +18 -4
  32. package/lib/routes/accounts.js +203 -0
  33. package/lib/routes/oauth.js +44 -2
  34. package/lib/routes/proxy.js +14 -0
  35. package/lib/routes/status.js +99 -5
  36. package/lib/scope-inspector.js +51 -0
  37. package/lib/stream-rotate.js +31 -10
  38. package/lib/updater.js +21 -0
  39. package/lib/vault-sync.js +412 -0
  40. package/lib/vault.js +1 -1
  41. package/lib/vendor-factory.js +6 -2
  42. package/lib/vendors/antigravity.js +11 -2
  43. package/lib/vendors/claude.js +56 -8
  44. package/lib/vendors/codex.js +34 -6
  45. package/lib/vendors/copilot-cli.js +8 -1
  46. package/lib/vendors/ernie.js +9 -2
  47. package/lib/vendors/grok.js +66 -6
  48. package/lib/vendors/jetbrains.js +133 -13
  49. package/lib/vendors/perplexity.js +148 -17
  50. package/package.json +1 -1
@@ -0,0 +1,379 @@
1
+ // #557: Headless Auth Helper with SSH Tunnel Command Generator and Terminal QR Code
2
+ // Assists users logging in to OAuth providers on remote headless servers (VPS, Docker, MiniPC).
3
+
4
+ import { escapeHtml } from './http.js'
5
+
6
+ // --- QR Code Generator (Byte Mode, ECC Level L, Versions 1-10) ---
7
+
8
+ const EXP = new Uint8Array(512)
9
+ const LOG = new Uint8Array(256)
10
+ for (let i = 0, x = 1; i < 255; i++) {
11
+ EXP[i] = x
12
+ LOG[x] = i
13
+ x = (x << 1) ^ (x >= 128 ? 0x11d : 0)
14
+ }
15
+ for (let i = 255; i < 512; i++) {
16
+ EXP[i] = EXP[i - 255]
17
+ }
18
+
19
+ function gfMul(x, y) {
20
+ return (x === 0 || y === 0) ? 0 : EXP[LOG[x] + LOG[y]]
21
+ }
22
+
23
+ function polyMul(p1, p2) {
24
+ const r = new Uint8Array(p1.length + p2.length - 1)
25
+ for (let i = 0; i < p1.length; i++) {
26
+ for (let j = 0; j < p2.length; j++) {
27
+ r[i + j] ^= gfMul(p1[i], p2[j])
28
+ }
29
+ }
30
+ return r
31
+ }
32
+
33
+ function rsGenPoly(deg) {
34
+ let p = new Uint8Array([1])
35
+ for (let i = 0; i < deg; i++) {
36
+ p = polyMul(p, new Uint8Array([1, EXP[i]]))
37
+ }
38
+ return p
39
+ }
40
+
41
+ function rsCompute(data, ecCount) {
42
+ const gen = rsGenPoly(ecCount)
43
+ const out = new Uint8Array(ecCount)
44
+ for (let i = 0; i < data.length; i++) {
45
+ const factor = data[i] ^ out[0]
46
+ for (let j = 0; j < ecCount - 1; j++) {
47
+ out[j] = out[j + 1] ^ gfMul(gen[j + 1], factor)
48
+ }
49
+ out[ecCount - 1] = gfMul(gen[ecCount], factor)
50
+ }
51
+ return out
52
+ }
53
+
54
+ const V_INFO = [
55
+ null,
56
+ { v: 1, total: 26, ec: 7, b: 1, align: [] },
57
+ { v: 2, total: 44, ec: 10, b: 1, align: [6, 18] },
58
+ { v: 3, total: 70, ec: 15, b: 1, align: [6, 22] },
59
+ { v: 4, total: 100, ec: 20, b: 1, align: [6, 26] },
60
+ { v: 5, total: 134, ec: 26, b: 1, align: [6, 30] },
61
+ { v: 6, total: 172, ec: 18, b: 2, align: [6, 34] },
62
+ { v: 7, total: 196, ec: 20, b: 2, align: [6, 22, 38] },
63
+ { v: 8, total: 242, ec: 24, b: 2, align: [6, 24, 42] },
64
+ { v: 9, total: 292, ec: 30, b: 2, align: [6, 26, 46] },
65
+ { v: 10, total: 346, ec: 18, b: 4, align: [6, 28, 50] },
66
+ ]
67
+
68
+ export function encodeQrMatrix(text) {
69
+ const str = String(text || '')
70
+ const data = Buffer.from(str, 'utf8')
71
+ let ver = 1
72
+ while (ver <= 10) {
73
+ const info = V_INFO[ver]
74
+ const dataCap = info.total - info.ec * info.b
75
+ const lenBits = ver < 10 ? 8 : 16
76
+ const requiredBits = 4 + lenBits + data.length * 8
77
+ if (requiredBits <= dataCap * 8) break
78
+ ver++
79
+ }
80
+ if (ver > 10) throw new Error('text too long for QR encoder')
81
+
82
+ const info = V_INFO[ver]
83
+ const totalDataBytes = info.total - info.ec * info.b
84
+ const bits = []
85
+ function pushBits(val, len) {
86
+ for (let i = len - 1; i >= 0; i--) bits.push((val >> i) & 1)
87
+ }
88
+ pushBits(4, 4)
89
+ pushBits(data.length, ver < 10 ? 8 : 16)
90
+ for (const b of data) pushBits(b, 8)
91
+ const rem = totalDataBytes * 8 - bits.length
92
+ pushBits(0, Math.min(4, rem))
93
+ while (bits.length % 8 !== 0) bits.push(0)
94
+ const padBytes = [0xec, 0x11]
95
+ let pIdx = 0
96
+ while (bits.length < totalDataBytes * 8) {
97
+ pushBits(padBytes[pIdx++ % 2], 8)
98
+ }
99
+
100
+ const dataBytes = new Uint8Array(totalDataBytes)
101
+ for (let i = 0; i < totalDataBytes; i++) {
102
+ let byte = 0
103
+ for (let j = 0; j < 8; j++) byte = (byte << 1) | bits[i * 8 + j]
104
+ dataBytes[i] = byte
105
+ }
106
+
107
+ const blockSize = Math.floor(totalDataBytes / info.b)
108
+ const ecBlocks = []
109
+ const dataBlocks = []
110
+ for (let b = 0; b < info.b; b++) {
111
+ const start = b * blockSize
112
+ const slice = dataBytes.subarray(start, start + blockSize)
113
+ dataBlocks.push(slice)
114
+ ecBlocks.push(rsCompute(slice, info.ec))
115
+ }
116
+
117
+ const finalCodewords = []
118
+ for (let i = 0; i < blockSize; i++) {
119
+ for (let b = 0; b < info.b; b++) finalCodewords.push(dataBlocks[b][i])
120
+ }
121
+ for (let i = 0; i < info.ec; i++) {
122
+ for (let b = 0; b < info.b; b++) finalCodewords.push(ecBlocks[b][i])
123
+ }
124
+
125
+ const size = 17 + 4 * ver
126
+ const matrix = Array.from({ length: size }, () => new Int8Array(size).fill(-1))
127
+ const isFunc = Array.from({ length: size }, () => new Uint8Array(size))
128
+
129
+ function setFunc(r, c, val) {
130
+ if (r >= 0 && r < size && c >= 0 && c < size) {
131
+ matrix[r][c] = val
132
+ isFunc[r][c] = 1
133
+ }
134
+ }
135
+
136
+ function drawFinder(r0, c0) {
137
+ for (let r = -1; r <= 7; r++) {
138
+ for (let c = -1; c <= 7; c++) {
139
+ const nr = r0 + r
140
+ const nc = c0 + c
141
+ if (nr < 0 || nr >= size || nc < 0 || nc >= size) continue
142
+ if (r === -1 || r === 7 || c === -1 || c === 7) {
143
+ setFunc(nr, nc, 0)
144
+ } else if (r === 0 || r === 6 || c === 0 || c === 6 || (r >= 2 && r <= 4 && c >= 2 && c <= 4)) {
145
+ setFunc(nr, nc, 1)
146
+ } else {
147
+ setFunc(nr, nc, 0)
148
+ }
149
+ }
150
+ }
151
+ }
152
+ drawFinder(0, 0)
153
+ drawFinder(0, size - 7)
154
+ drawFinder(size - 7, 0)
155
+
156
+ for (let i = 8; i < size - 8; i++) {
157
+ setFunc(6, i, i % 2 === 0 ? 1 : 0)
158
+ setFunc(i, 6, i % 2 === 0 ? 1 : 0)
159
+ }
160
+
161
+ const align = info.align
162
+ for (let i = 0; i < align.length; i++) {
163
+ for (let j = 0; j < align.length; j++) {
164
+ const ar = align[i]
165
+ const ac = align[j]
166
+ if ((ar === 6 && ac === 6) || (ar === 6 && ac === size - 7) || (ar === size - 7 && ac === 6)) continue
167
+ for (let r = -2; r <= 2; r++) {
168
+ for (let c = -2; c <= 2; c++) {
169
+ const val = (Math.max(Math.abs(r), Math.abs(c)) !== 1) ? 1 : 0
170
+ setFunc(ar + r, ac + c, val)
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ setFunc(4 * ver + 9, 8, 1)
177
+
178
+ for (let i = 0; i < 9; i++) {
179
+ if (!isFunc[8][i]) setFunc(8, i, 0)
180
+ if (!isFunc[i][8]) setFunc(i, 8, 0)
181
+ }
182
+ for (let i = 0; i < 8; i++) {
183
+ if (!isFunc[8][size - 1 - i]) setFunc(8, size - 1 - i, 0)
184
+ if (!isFunc[size - 1 - i][8]) setFunc(size - 1 - i, 8, 0)
185
+ }
186
+
187
+ const allBits = []
188
+ for (const cw of finalCodewords) {
189
+ for (let i = 7; i >= 0; i--) allBits.push((cw >> i) & 1)
190
+ }
191
+ const remBits = [0, 0, 7, 7, 7, 7, 7, 0, 0, 0, 0][ver] || 0
192
+ for (let i = 0; i < remBits; i++) allBits.push(0)
193
+
194
+ let bitIdx = 0
195
+ let dir = -1
196
+ let c = size - 1
197
+ while (c > 0) {
198
+ if (c === 6) c--
199
+ for (let rStep = 0; rStep < size; rStep++) {
200
+ const r = dir === -1 ? (size - 1 - rStep) : rStep
201
+ for (let dc = 0; dc < 2; dc++) {
202
+ const col = c - dc
203
+ if (!isFunc[r][col]) {
204
+ const bit = bitIdx < allBits.length ? allBits[bitIdx++] : 0
205
+ const mask = ((r + col) % 2 === 0) ? 1 : 0
206
+ matrix[r][col] = bit ^ mask
207
+ }
208
+ }
209
+ }
210
+ dir = -dir
211
+ c -= 2
212
+ }
213
+
214
+ const formatBits = 0x77c4
215
+ for (let i = 0; i < 15; i++) {
216
+ const bit = (formatBits >> (14 - i)) & 1
217
+ if (i < 6) matrix[8][i] = bit
218
+ else if (i === 6) matrix[8][7] = bit
219
+ else if (i === 7) matrix[8][8] = bit
220
+ else if (i === 8) matrix[7][8] = bit
221
+ else matrix[14 - i][8] = bit
222
+
223
+ if (i < 8) matrix[size - 1 - i][8] = bit
224
+ else matrix[8][size - 15 + i] = bit
225
+ }
226
+
227
+ return matrix
228
+ }
229
+
230
+ export function renderAsciiQr(text) {
231
+ const m = encodeQrMatrix(text)
232
+ const size = m.length
233
+ const border = 1
234
+ const lines = []
235
+ for (let r = -border; r < size + border; r += 2) {
236
+ let line = ''
237
+ for (let c = -border; c < size + border; c++) {
238
+ const top = (r >= 0 && r < size && c >= 0 && c < size) ? m[r][c] : 0
239
+ const bot = (r + 1 >= 0 && r + 1 < size && c >= 0 && c < size) ? m[r + 1][c] : 0
240
+ if (top && bot) line += '█'
241
+ else if (top && !bot) line += '▀'
242
+ else if (!top && bot) line += '▄'
243
+ else line += ' '
244
+ }
245
+ lines.push(line)
246
+ }
247
+ return lines.join('\n')
248
+ }
249
+
250
+ export function renderSvgQr(text, { size = 200, border = 2, color = '#000000', bg = '#ffffff' } = {}) {
251
+ const m = encodeQrMatrix(text)
252
+ const n = m.length
253
+ const total = n + border * 2
254
+ let paths = ''
255
+ for (let r = 0; r < n; r++) {
256
+ for (let c = 0; c < n; c++) {
257
+ if (m[r][c]) {
258
+ paths += `M${c + border},${r + border}h1v1h-1z `
259
+ }
260
+ }
261
+ }
262
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${total} ${total}" width="${size}" height="${size}" shape-rendering="crispEdges">` +
263
+ `<rect width="${total}" height="${total}" fill="${bg}"/>` +
264
+ `<path d="${paths.trim()}" fill="${color}"/>` +
265
+ `</svg>`
266
+ }
267
+
268
+ export function renderSvgDataUrl(text, options) {
269
+ const svg = renderSvgQr(text, options)
270
+ return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg)
271
+ }
272
+
273
+ // --- SSH Tunnel Command Generator ---
274
+
275
+ export function generateSshTunnelCommand({ port = 1455, host = '', user = '' } = {}) {
276
+ const cleanPort = Number(port) || 1455
277
+ let cleanHost = String(host || '').trim()
278
+ if (!cleanHost || cleanHost === '127.0.0.1' || cleanHost === 'localhost' || cleanHost === '::1') {
279
+ cleanHost = '<remote-host>'
280
+ }
281
+ // Strip existing port if passed from req.headers.host
282
+ if (cleanHost.includes(':')) {
283
+ cleanHost = cleanHost.split(':')[0]
284
+ }
285
+ const cleanUser = String(user || '').trim() || (process.env.USER || process.env.USERNAME || '<user>')
286
+ return `ssh -N -L ${cleanPort}:127.0.0.1:${cleanPort} ${cleanUser}@${cleanHost}`
287
+ }
288
+
289
+ export function getHeadlessAuthInfo({ provider = '', redirectUri = '', authUrl = '', host = '', user = '' } = {}) {
290
+ let loopbackPort = 1455
291
+ let isLoopback = false
292
+ if (redirectUri) {
293
+ try {
294
+ const u = new URL(redirectUri)
295
+ if (u.hostname === 'localhost' || u.hostname === '127.0.0.1') {
296
+ isLoopback = true
297
+ loopbackPort = Number(u.port) || 1455
298
+ }
299
+ } catch { /* ignore malformed */ }
300
+ } else if (provider === 'grok') {
301
+ loopbackPort = 56121
302
+ isLoopback = true
303
+ } else if (provider === 'codex') {
304
+ loopbackPort = 1455
305
+ isLoopback = true
306
+ }
307
+
308
+ const sshTunnelCommand = generateSshTunnelCommand({ port: loopbackPort, host, user })
309
+ let terminalQr = ''
310
+ let svgDataUrl = ''
311
+ if (authUrl) {
312
+ try {
313
+ terminalQr = renderAsciiQr(authUrl)
314
+ svgDataUrl = renderSvgDataUrl(authUrl)
315
+ } catch { /* ignore length limits */ }
316
+ }
317
+
318
+ return {
319
+ provider,
320
+ isLoopback,
321
+ loopbackPort,
322
+ sshTunnelCommand,
323
+ authUrl,
324
+ terminalQr,
325
+ svgDataUrl,
326
+ }
327
+ }
328
+
329
+ export async function handleHeadlessRedirect(req, res, { pending, completeOAuth, accountsView }) {
330
+ const rawUrl = new URL(req.url || '/', 'http://127.0.0.1')
331
+ const code = rawUrl.searchParams.get('code') || ''
332
+ const state = rawUrl.searchParams.get('state') || ''
333
+ const error = rawUrl.searchParams.get('error') || ''
334
+ const errorDesc = rawUrl.searchParams.get('error_description') || error
335
+
336
+ if (error) {
337
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' })
338
+ res.end(`<!doctype html><meta charset="utf-8"><title>OAuth Error</title><body style="font-family:sans-serif;padding:2rem;">` +
339
+ `<h2>Authorization Failed</h2><p style="color:#e53935;">${escapeHtml(errorDesc)}</p>` +
340
+ `<p>You can close this tab and return to Settings.</p></body>`)
341
+ return
342
+ }
343
+
344
+ if (!code) {
345
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' })
346
+ res.end(`<!doctype html><meta charset="utf-8"><title>Missing Code</title><body style="font-family:sans-serif;padding:2rem;">` +
347
+ `<h2>Missing Authorization Code</h2><p>No authorization code found in callback query.</p></body>`)
348
+ return
349
+ }
350
+
351
+ const row = pending ? pending.get(state) : null
352
+ if (row && typeof completeOAuth === 'function') {
353
+ try {
354
+ await completeOAuth({ provider: row.provider, index: row.index, code, state })
355
+ if (typeof accountsView === 'function') await accountsView()
356
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
357
+ res.end(`<!doctype html><meta charset="utf-8"><title>Subscriptions</title><body style="font-family:sans-serif;padding:2rem;">` +
358
+ `<h2 style="color:#2e7d32;">Signed In Successfully</h2>` +
359
+ `<p>Your account has been authorized. You can close this window and return to Settings.</p>` +
360
+ `<script>if (window.opener) { window.opener.postMessage({ type: 'dsh-oauth-success', state: '${escapeHtml(state)}' }, '*'); }</script>` +
361
+ `</body>`)
362
+ return
363
+ } catch (e) {
364
+ res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' })
365
+ res.end(`<!doctype html><meta charset="utf-8"><title>OAuth Exchange Failed</title><body style="font-family:sans-serif;padding:2rem;">` +
366
+ `<h2>Failed to Complete OAuth</h2><p style="color:#e53935;">${escapeHtml(String(e && e.message || e))}</p></body>`)
367
+ return
368
+ }
369
+ }
370
+
371
+ // Fallback: pending state expired or missing (show copyable code)
372
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
373
+ res.end(`<!doctype html><meta charset="utf-8"><title>Authorization Code</title><body style="font-family:sans-serif;padding:2rem;">` +
374
+ `<h2>Authorization Code Received</h2>` +
375
+ `<p>Session timed out or running in manual mode. Copy the code or URL below and paste it into Settings &gt; Connect:</p>` +
376
+ `<textarea readonly style="width:100%;height:80px;font-family:monospace;padding:0.5rem;">${escapeHtml(rawUrl.toString())}</textarea>` +
377
+ `<p style="margin-top:1rem;"><strong>Code:</strong> <code>${escapeHtml(code)}</code></p>` +
378
+ `</body>`)
379
+ }
package/lib/history.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { calculateValueDelivered } from './pricing.js'
1
2
  import { calculateAnalytics } from './analytics.js'
2
3
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
3
4
  import { join } from 'node:path'
@@ -126,7 +127,9 @@ export class HistoryStore {
126
127
  let tpsSum = 0
127
128
  let tpsCount = 0
128
129
  let lastTps = null
130
+ let totalPromptTokens = 0
129
131
  let totalOutputTokens = 0
132
+ let totalCacheReadTokens = 0
130
133
  let latencySum = 0
131
134
  let lastLatency = 0
132
135
 
@@ -147,9 +150,15 @@ export class HistoryStore {
147
150
  tpsCount++
148
151
  if (lastTps === null) lastTps = r.tps
149
152
  }
153
+ if (typeof r.inputTokens === "number" && r.inputTokens > 0) {
154
+ totalPromptTokens += r.inputTokens
155
+ }
150
156
  if (typeof r.outputTokens === "number" && r.outputTokens > 0) {
151
157
  totalOutputTokens += r.outputTokens
152
158
  }
159
+ if (typeof r.cacheReadTokens === "number" && r.cacheReadTokens > 0) {
160
+ totalCacheReadTokens += r.cacheReadTokens
161
+ }
153
162
  }
154
163
  if (count === 0) return null
155
164
  return {
@@ -160,7 +169,14 @@ export class HistoryStore {
160
169
  lastTtftMs: lastTtft,
161
170
  avgTps: tpsCount > 0 ? Math.round((tpsSum / tpsCount) * 10) / 10 : null,
162
171
  lastTps: lastTps,
172
+ totalPromptTokens,
163
173
  totalOutputTokens,
174
+ totalCacheReadTokens,
175
+ valueDelivered: calculateValueDelivered({
176
+ promptTokens: totalPromptTokens,
177
+ completionTokens: totalOutputTokens,
178
+ cacheReadTokens: totalCacheReadTokens,
179
+ }),
164
180
  }
165
181
  }
166
182
 
@@ -179,7 +195,9 @@ export class HistoryStore {
179
195
  tpsSum: 0,
180
196
  tpsCount: 0,
181
197
  lastTps: null,
198
+ totalPromptTokens: 0,
182
199
  totalOutputTokens: 0,
200
+ totalCacheReadTokens: 0,
183
201
  }
184
202
  }
185
203
  const st = map[r.ref]
@@ -198,9 +216,15 @@ export class HistoryStore {
198
216
  st.tpsCount++
199
217
  if (st.lastTps === null) st.lastTps = r.tps
200
218
  }
219
+ if (typeof r.inputTokens === "number" && r.inputTokens > 0) {
220
+ st.totalPromptTokens += r.inputTokens
221
+ }
201
222
  if (typeof r.outputTokens === "number" && r.outputTokens > 0) {
202
223
  st.totalOutputTokens += r.outputTokens
203
224
  }
225
+ if (typeof r.cacheReadTokens === "number" && r.cacheReadTokens > 0) {
226
+ st.totalCacheReadTokens += r.cacheReadTokens
227
+ }
204
228
  }
205
229
  const out = {}
206
230
  for (const [ref, st] of Object.entries(map)) {
@@ -212,7 +236,14 @@ export class HistoryStore {
212
236
  lastTtftMs: st.lastTtft,
213
237
  avgTps: st.tpsCount > 0 ? Math.round((st.tpsSum / st.tpsCount) * 10) / 10 : null,
214
238
  lastTps: st.lastTps,
239
+ totalPromptTokens: st.totalPromptTokens,
215
240
  totalOutputTokens: st.totalOutputTokens,
241
+ totalCacheReadTokens: st.totalCacheReadTokens,
242
+ valueDelivered: calculateValueDelivered({
243
+ promptTokens: st.totalPromptTokens,
244
+ completionTokens: st.totalOutputTokens,
245
+ cacheReadTokens: st.totalCacheReadTokens,
246
+ }),
216
247
  }
217
248
  }
218
249
  return out
@@ -296,6 +327,11 @@ export class HistoryStore {
296
327
  ? Math.round((totalCacheReadTokens / totalPromptTokens) * 1000) / 10
297
328
  : 0,
298
329
  accounts: this.accountsTelemetry(),
330
+ valueDelivered: calculateValueDelivered({
331
+ promptTokens: totalPromptTokens,
332
+ completionTokens: totalCompletionTokens,
333
+ cacheReadTokens: totalCacheReadTokens,
334
+ }),
299
335
  recentDecisions,
300
336
  }
301
337
  }
package/lib/http.js CHANGED
@@ -103,7 +103,17 @@ export function isTrustedSettingsRequest(request, targetCtx) {
103
103
  if (!host || u.host.toLowerCase() !== host.toLowerCase()) {
104
104
  return false
105
105
  }
106
- } else if (u.protocol !== 'dsh-app:') {
106
+ } else if (u.protocol === 'dsh-app:') {
107
+ const appHost = u.host.toLowerCase()
108
+ if (appHost && appHost !== 'app' && appHost !== 'localhost') {
109
+ return false
110
+ }
111
+ const remoteAddr = request?.socket?.remoteAddress || request?.connection?.remoteAddress
112
+ const hasRemote = Boolean(remoteAddr || request?.socket || request?.connection)
113
+ if (hasRemote && !isLoopback(request)) {
114
+ return false
115
+ }
116
+ } else {
107
117
  return false
108
118
  }
109
119
  } catch {
@@ -120,7 +130,17 @@ export function isTrustedSettingsRequest(request, targetCtx) {
120
130
  if (!host || u.host.toLowerCase() !== host.toLowerCase()) {
121
131
  return false
122
132
  }
123
- } else if (u.protocol !== 'dsh-app:') {
133
+ } else if (u.protocol === 'dsh-app:') {
134
+ const appHost = u.host.toLowerCase()
135
+ if (appHost && appHost !== 'app' && appHost !== 'localhost') {
136
+ return false
137
+ }
138
+ const remoteAddr = request?.socket?.remoteAddress || request?.connection?.remoteAddress
139
+ const hasRemote = Boolean(remoteAddr || request?.socket || request?.connection)
140
+ if (hasRemote && !isLoopback(request)) {
141
+ return false
142
+ }
143
+ } else {
124
144
  return false
125
145
  }
126
146
  } catch {
@@ -177,7 +197,8 @@ export function isTrustedSettingsRequest(request, targetCtx) {
177
197
  if (u.protocol === 'http:' || u.protocol === 'https:') {
178
198
  if (u.host.toLowerCase() === host.toLowerCase()) return true
179
199
  } else if (u.protocol === 'dsh-app:') {
180
- return true
200
+ const appHost = u.host.toLowerCase()
201
+ if (!appHost || appHost === 'app' || appHost === 'localhost') return true
181
202
  }
182
203
  } catch { /* bestEffort */ }
183
204
  }
@@ -187,7 +208,8 @@ export function isTrustedSettingsRequest(request, targetCtx) {
187
208
  if (u.protocol === 'http:' || u.protocol === 'https:') {
188
209
  if (u.host.toLowerCase() === host.toLowerCase()) return true
189
210
  } else if (u.protocol === 'dsh-app:') {
190
- return true
211
+ const appHost = u.host.toLowerCase()
212
+ if (!appHost || appHost === 'app' || appHost === 'localhost') return true
191
213
  }
192
214
  } catch { /* bestEffort */ }
193
215
  }