@goodandready/dsh-subscriptions 0.6.72 → 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.
- package/README.md +7 -0
- package/README.ru.md +7 -0
- package/README.zh.md +7 -0
- package/lib/accounts-view.js +97 -5
- package/lib/accounts.js +137 -11
- package/lib/adapter.js +3 -0
- package/lib/alerts.js +35 -0
- package/lib/analytics.js +31 -2
- package/lib/billing.js +99 -0
- package/lib/blob.js +18 -0
- package/lib/capabilities.js +208 -0
- package/lib/client.js +344 -17
- package/lib/config-schema.js +46 -4
- package/lib/diagnostic-wizard.js +2 -5
- package/lib/diagnostics.js +2 -2
- package/lib/error-buffer.js +2 -2
- package/lib/forecast.js +56 -0
- package/lib/headless-auth.js +379 -0
- package/lib/history.js +36 -0
- package/lib/http.js +26 -4
- package/lib/index.js +98 -11
- package/lib/jwt.js +84 -17
- package/lib/loopback.js +3 -2
- package/lib/mask.js +16 -0
- package/lib/oauth-pending.js +12 -0
- package/lib/pricing.js +89 -0
- package/lib/refs.js +2 -0
- package/lib/reset-clock.js +55 -0
- package/lib/rotate.js +18 -4
- package/lib/routes/accounts.js +131 -0
- package/lib/routes/oauth.js +44 -2
- package/lib/routes/proxy.js +14 -0
- package/lib/scope-inspector.js +51 -0
- package/lib/stream-rotate.js +31 -10
- package/lib/updater.js +21 -0
- package/lib/vault-sync.js +412 -0
- package/lib/vault.js +1 -1
- package/lib/vendor-factory.js +6 -2
- package/lib/vendors/antigravity.js +11 -2
- package/lib/vendors/claude.js +56 -8
- package/lib/vendors/codex.js +34 -6
- package/lib/vendors/copilot-cli.js +8 -1
- package/lib/vendors/ernie.js +9 -2
- package/lib/vendors/grok.js +66 -6
- package/lib/vendors/jetbrains.js +133 -13
- package/lib/vendors/perplexity.js +148 -17
- package/package.json +1 -1
package/lib/diagnostic-wizard.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Guided Diagnostic Wizard for accounts, credentials, and network connectivity (#519).
|
|
2
2
|
// Enforces 0 Cyrillic characters.
|
|
3
3
|
|
|
4
|
-
import { maskEmail,
|
|
4
|
+
import { maskEmail, maskSecrets, cleanErrorMessage } from './mask.js'
|
|
5
5
|
import { getVendor } from './vendors/index.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -11,10 +11,7 @@ import { getVendor } from './vendors/index.js'
|
|
|
11
11
|
*/
|
|
12
12
|
export function sanitizeDiagnosticData(val) {
|
|
13
13
|
if (typeof val === 'string') {
|
|
14
|
-
return
|
|
15
|
-
.replace(/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, 'Bearer [REDACTED]')
|
|
16
|
-
.replace(/key=[A-Za-z0-9._~+/-]+/gi, 'key=[REDACTED]')
|
|
17
|
-
.replace(/token=[A-Za-z0-9._~+/-]+/gi, 'token=[REDACTED]')
|
|
14
|
+
return maskSecrets(val)
|
|
18
15
|
}
|
|
19
16
|
if (Array.isArray(val)) {
|
|
20
17
|
return val.map(sanitizeDiagnosticData)
|
package/lib/diagnostics.js
CHANGED
|
@@ -3,13 +3,13 @@ import { getSessionReasoningStats } from './reasoning-effort.js'
|
|
|
3
3
|
import { defaultConcurrencyQueue } from './concurrency-queue.js'
|
|
4
4
|
import { calculateCacheMetrics } from './analytics.js'
|
|
5
5
|
import { getSessionPinStats } from './session-pin.js'
|
|
6
|
-
import {
|
|
6
|
+
import { maskSecrets } from './mask.js'
|
|
7
7
|
import { normalizeSlots } from './accounts.js'
|
|
8
8
|
import { PROVIDERS } from './refs.js'
|
|
9
9
|
import { defaultErrorBuffer } from './error-buffer.js'
|
|
10
10
|
|
|
11
11
|
function scrubReport(v) {
|
|
12
|
-
if (typeof v === 'string') return
|
|
12
|
+
if (typeof v === 'string') return maskSecrets(v)
|
|
13
13
|
if (Array.isArray(v)) return v.map(scrubReport)
|
|
14
14
|
if (v && typeof v === 'object') {
|
|
15
15
|
const o = {}
|
package/lib/error-buffer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cleanErrorMessage,
|
|
1
|
+
import { cleanErrorMessage, maskSecrets } from './mask.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Ring buffer holding the most recent runtime errors across vendors and streams.
|
|
@@ -19,7 +19,7 @@ export class ErrorRingBuffer {
|
|
|
19
19
|
* @param {{ provider?: string, ref?: string, model?: string, status?: number|null, code?: string, message?: string }} [param0]
|
|
20
20
|
*/
|
|
21
21
|
record({ provider = 'unknown', ref = '', model = '', status = null, code = '', message = '' } = {}) {
|
|
22
|
-
const rawMsg = cleanErrorMessage(
|
|
22
|
+
const rawMsg = cleanErrorMessage(maskSecrets(message))
|
|
23
23
|
const entry = {
|
|
24
24
|
id: String(this._nextId++),
|
|
25
25
|
ts: new Date().toISOString(),
|
package/lib/forecast.js
CHANGED
|
@@ -141,3 +141,59 @@ export function calculateBurnUrgency(remainingPercent, resetAt, nowMs = Date.now
|
|
|
141
141
|
const clampedHours = Math.max(hoursUntilReset, 0.05)
|
|
142
142
|
return rem / clampedHours
|
|
143
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* #559: Calculates remaining runway duration and pacing status based on burn rate.
|
|
147
|
+
* @param {number|null} remainingPercent
|
|
148
|
+
* @param {number} pacePerHour
|
|
149
|
+
* @param {number|null} [_resetAt]
|
|
150
|
+
* @param {number} [_nowMs]
|
|
151
|
+
* @returns {{ runwayMs: number|null, runwayHours: number|null, runwayText: string, pacingStatus: string }}
|
|
152
|
+
*/
|
|
153
|
+
export function calculatePacingRunway(remainingPercent, pacePerHour, _resetAt = null, _nowMs = Date.now()) {
|
|
154
|
+
if (remainingPercent === null || remainingPercent === undefined) {
|
|
155
|
+
return { runwayMs: null, runwayHours: null, runwayText: '', pacingStatus: 'unknown' }
|
|
156
|
+
}
|
|
157
|
+
const rem = Number(remainingPercent)
|
|
158
|
+
if (!Number.isFinite(rem)) {
|
|
159
|
+
return { runwayMs: null, runwayHours: null, runwayText: '', pacingStatus: 'unknown' }
|
|
160
|
+
}
|
|
161
|
+
if (rem <= 0) {
|
|
162
|
+
return { runwayMs: 0, runwayHours: 0, runwayText: '0m (exhausted)', pacingStatus: 'critical' }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const pace = Number(pacePerHour) || 0
|
|
166
|
+
if (pace <= 0.02) {
|
|
167
|
+
return { runwayMs: Infinity, runwayHours: Infinity, runwayText: 'Sustainable', pacingStatus: 'sustainable' }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const runwayHours = rem / pace
|
|
171
|
+
const runwayMs = Math.round(runwayHours * HOUR_MS)
|
|
172
|
+
|
|
173
|
+
let runwayText = ''
|
|
174
|
+
if (runwayHours >= 24) {
|
|
175
|
+
const days = Math.round(runwayHours / 24)
|
|
176
|
+
runwayText = `~${days}d`
|
|
177
|
+
} else if (runwayHours >= 1) {
|
|
178
|
+
const h = Math.floor(runwayHours)
|
|
179
|
+
const m = Math.round((runwayHours % 1) * 60)
|
|
180
|
+
runwayText = m > 0 ? `~${h}h ${m}m` : `~${h}h`
|
|
181
|
+
} else {
|
|
182
|
+
const m = Math.max(1, Math.round(runwayHours * 60))
|
|
183
|
+
runwayText = m <= 15 ? '< 15m' : `~${m}m`
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let pacingStatus = 'sustainable'
|
|
187
|
+
if (runwayHours < 0.5) {
|
|
188
|
+
pacingStatus = 'critical'
|
|
189
|
+
} else if (runwayHours <= 2.5) {
|
|
190
|
+
pacingStatus = 'elevated'
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
runwayMs,
|
|
195
|
+
runwayHours: Math.round(runwayHours * 10) / 10,
|
|
196
|
+
runwayText,
|
|
197
|
+
pacingStatus,
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -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 > 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
211
|
+
const appHost = u.host.toLowerCase()
|
|
212
|
+
if (!appHost || appHost === 'app' || appHost === 'localhost') return true
|
|
191
213
|
}
|
|
192
214
|
} catch { /* bestEffort */ }
|
|
193
215
|
}
|