@goodandready/dsh-subscriptions 0.7.9 → 0.7.10

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.
@@ -105,6 +105,8 @@ export function formatDoctor(payload) {
105
105
  lines.push('doctor: ' + (doctorFailed(payload) ? 'degraded' : 'ok'))
106
106
  lines.push('errors: ' + errors + ' conflicts: ' + conflicts)
107
107
  lines.push('key-rotation: ' + (kr.available ? (kr.healthyKeys + '/' + kr.totalKeys + ' healthy keys') : ('unavailable' + (kr.reason ? ' (' + kr.reason + ')' : ''))))
108
+ const snapshots = diag.vaultSnapshots || {}
109
+ lines.push('snapshots: ' + (snapshots.count || 0) + ' (' + (snapshots.bytes || 0) + ' bytes)')
108
110
  for (const row of (sources.providers || [])) lines.push(row.provider + ': ' + (row.selected || 'none'))
109
111
  return lines.join('\n')
110
112
  }
@@ -0,0 +1,21 @@
1
+ // #745: health is about having a usable source, not about uptime alone.
2
+ export function assessHealth({ accounts = [], sources = null, snapshots = null, webhookUrl = '', ollama = false } = {}) {
3
+ const list = Array.isArray(accounts) ? accounts : []
4
+ const connectedAccounts = list.filter((a) => a && a.configured).length
5
+ const sourceRows = sources && Array.isArray(sources.providers) ? sources.providers : []
6
+ const selectedSources = sourceRows.filter((row) => row && row.selected).length
7
+ const keyRotation = Boolean(sources && sources.keyRotation && sources.keyRotation.available)
8
+ const ok = connectedAccounts > 0 || keyRotation || Boolean(ollama)
9
+ return {
10
+ ok,
11
+ status: ok ? 'healthy' : 'degraded',
12
+ checks: {
13
+ connectedAccounts,
14
+ keyRotation,
15
+ ollama: Boolean(ollama),
16
+ webhook: Boolean(webhookUrl),
17
+ snapshots: snapshots ? Number(snapshots.count) || 0 : 0,
18
+ selectedSources,
19
+ },
20
+ }
21
+ }
package/lib/metrics.js ADDED
@@ -0,0 +1,62 @@
1
+ // #744: Prometheus text exposition. No secrets, labels escaped, deterministic order.
2
+ function label(value) {
3
+ return String(value == null ? '' : value).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')
4
+ }
5
+
6
+ function add(lines, name, help, type, samples) {
7
+ lines.push('# HELP ' + name + ' ' + help)
8
+ lines.push('# TYPE ' + name + ' ' + type)
9
+ for (const sample of samples) lines.push(name + (sample.labels ? '{' + sample.labels + '}' : '') + ' ' + sample.value)
10
+ }
11
+
12
+ export function formatMetrics({ accounts = [], rows = [], alerts = [], sources = null } = {}) {
13
+ const lines = []
14
+ const accountRows = Array.isArray(accounts) ? accounts : []
15
+ add(lines, 'dsh_subscriptions_connected_accounts', 'Accounts with a configured credential.', 'gauge', [
16
+ { value: accountRows.filter((a) => a && a.configured).length },
17
+ ])
18
+ const configured = []
19
+ const usage = []
20
+ const cooldown = []
21
+ for (const account of accountRows) {
22
+ if (!account || !account.provider) continue
23
+ const labels = 'provider="' + label(account.provider) + '",index="' + label(account.index || 1) + '"'
24
+ configured.push({ labels, value: account.configured ? 1 : 0 })
25
+ if (account.usagePercent != null) usage.push({ labels, value: Number(account.usagePercent) })
26
+ const until = Number(account.cooldownUntil) || 0
27
+ cooldown.push({ labels, value: until > Date.now() ? Math.round((until - Date.now()) / 1000) : 0 })
28
+ }
29
+ add(lines, 'dsh_subscriptions_account_configured', '1 when the slot has a credential.', 'gauge', configured)
30
+ add(lines, 'dsh_subscriptions_account_usage_percent', 'Last observed quota usage.', 'gauge', usage)
31
+ add(lines, 'dsh_subscriptions_account_cooldown_seconds', 'Remaining cooldown.', 'gauge', cooldown)
32
+
33
+ const selected = []
34
+ for (const row of (sources && Array.isArray(sources.providers) ? sources.providers : [])) {
35
+ if (!row || !row.selected) continue
36
+ selected.push({ labels: 'provider="' + label(row.provider) + '",source="' + label(row.selected) + '"', value: 1 })
37
+ }
38
+ add(lines, 'dsh_subscriptions_source_selected', 'Selected source for a provider.', 'gauge', selected)
39
+
40
+ const byRequest = new Map()
41
+ for (const row of (Array.isArray(rows) ? rows : [])) {
42
+ const provider = row && row.provider ? row.provider : 'unknown'
43
+ const status = row && row.status != null ? row.status : 'unknown'
44
+ const key = provider + '|' + status
45
+ byRequest.set(key, (byRequest.get(key) || 0) + 1)
46
+ }
47
+ add(lines, 'dsh_subscriptions_requests_total', 'Recorded requests by provider and status.', 'counter',
48
+ [...byRequest.entries()].sort().map(([key, value]) => {
49
+ const [provider, status] = key.split('|')
50
+ return { labels: 'provider="' + label(provider) + '",status="' + label(status) + '"', value }
51
+ }))
52
+
53
+ const bySeverity = new Map()
54
+ for (const alert of (Array.isArray(alerts) ? alerts : [])) {
55
+ const severity = alert && alert.severity ? alert.severity : 'warn'
56
+ bySeverity.set(severity, (bySeverity.get(severity) || 0) + 1)
57
+ }
58
+ add(lines, 'dsh_subscriptions_alerts_total', 'Recent alerts by severity.', 'counter',
59
+ [...bySeverity.entries()].sort().map(([severity, value]) => ({ labels: 'severity="' + label(severity) + '"', value })))
60
+
61
+ return lines.join('\n') + '\n'
62
+ }
@@ -0,0 +1,33 @@
1
+ import { getRecentAlerts } from '../alerts.js'
2
+ import { isLoopback, isTrustedSettingsRequest } from '../http.js'
3
+ import { formatMetrics } from '../metrics.js'
4
+ import { collectSourceChain } from './sources.js'
5
+
6
+ export function registerMetricsRoutes(ctx, state) {
7
+ ctx.effect(() => ctx.webServer.register({
8
+ kind: 'exact',
9
+ path: '/dsh-subscriptions/metrics',
10
+ handler: async (req, res) => {
11
+ if (req.method !== 'GET') {
12
+ res.writeHead(405, { 'content-type': 'text/plain; charset=utf-8' })
13
+ res.end('method not allowed\n')
14
+ return
15
+ }
16
+ if (!isLoopback(req) && !isTrustedSettingsRequest(req, ctx)) {
17
+ res.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' })
18
+ res.end('forbidden\n')
19
+ return
20
+ }
21
+ const accounts = await state.accountsView().catch(() => [])
22
+ const sources = await collectSourceChain(state, req.socket && req.socket.localPort)
23
+ const text = formatMetrics({
24
+ accounts,
25
+ rows: typeof state.history?.all === 'function' ? state.history.all() : [],
26
+ alerts: getRecentAlerts(1000),
27
+ sources,
28
+ })
29
+ res.writeHead(200, { 'content-type': 'text/plain; version=0.0.4; charset=utf-8' })
30
+ res.end(text)
31
+ },
32
+ }), 'dsh-subscriptions: /metrics')
33
+ }
@@ -1,8 +1,33 @@
1
1
  import { isTrustedSettingsRequest, safeJsonHandler, writeJson } from '../http.js'
2
2
  import { buildSourceChain, readKeyRotationStatus, subscriptionCandidate } from '../key-rotation-bridge.js'
3
3
 
4
- export function registerSourcesRoutes(ctx, state) {
4
+ export async function collectSourceChain(state, port) {
5
5
  const { accountsView, live } = state
6
+ let subscriptions = []
7
+ try {
8
+ const rows = await accountsView()
9
+ subscriptions = (rows || []).map(subscriptionCandidate)
10
+ } catch { /* keep the chain available */ }
11
+ const keyRotation = port
12
+ ? await readKeyRotationStatus({ baseUrl: 'http://127.0.0.1:' + port })
13
+ : { available: false, reason: 'no local port', providers: [], totalKeys: 0, healthyKeys: 0 }
14
+ const chain = buildSourceChain({
15
+ subscriptions,
16
+ keyRotation,
17
+ ollama: { available: live().ollamaFallback !== false },
18
+ })
19
+ return {
20
+ keyRotation: {
21
+ available: keyRotation.available,
22
+ reason: keyRotation.reason || null,
23
+ totalKeys: keyRotation.totalKeys,
24
+ healthyKeys: keyRotation.healthyKeys,
25
+ },
26
+ ...chain,
27
+ }
28
+ }
29
+
30
+ export function registerSourcesRoutes(ctx, state) {
6
31
 
7
32
  ctx.effect(() => ctx.webServer.register({
8
33
  kind: 'exact',
@@ -16,30 +41,8 @@ export function registerSourcesRoutes(ctx, state) {
16
41
  writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
17
42
  return
18
43
  }
19
- let subscriptions = []
20
- try {
21
- const rows = await accountsView()
22
- subscriptions = (rows || []).map(subscriptionCandidate)
23
- } catch { /* keep the chain available */ }
24
- const port = req.socket && req.socket.localPort
25
- const keyRotation = port
26
- ? await readKeyRotationStatus({ baseUrl: 'http://127.0.0.1:' + port })
27
- : { available: false, reason: 'no local port', providers: [], totalKeys: 0, healthyKeys: 0 }
28
- const chain = buildSourceChain({
29
- subscriptions,
30
- keyRotation,
31
- ollama: { available: live().ollamaFallback !== false },
32
- })
33
- writeJson(res, 200, {
34
- ok: true,
35
- keyRotation: {
36
- available: keyRotation.available,
37
- reason: keyRotation.reason || null,
38
- totalKeys: keyRotation.totalKeys,
39
- healthyKeys: keyRotation.healthyKeys,
40
- },
41
- ...chain,
42
- })
44
+ const payload = await collectSourceChain(state, req.socket && req.socket.localPort)
45
+ writeJson(res, 200, { ok: true, ...payload })
43
46
  }),
44
47
  }), 'dsh-subscriptions: /sources')
45
48
  }
@@ -26,6 +26,9 @@ import { quotaSnapshot } from '../ratelimit.js'
26
26
  import { PROVIDERS, displayName, droppedCredentialRefs, isProvider, oauthRef } from '../refs.js'
27
27
  import { getVendor } from '../vendors/index.js'
28
28
  import { defaultErrorBuffer } from '../error-buffer.js'
29
+ import { assessHealth } from '../health-report.js'
30
+ import { defaultSnapshotStore } from '../vault-snapshots.js'
31
+ import { collectSourceChain } from './sources.js'
29
32
 
30
33
  export function registerStatusRoutes(ctx, state) {
31
34
  const {
@@ -82,7 +85,16 @@ export function registerStatusRoutes(ctx, state) {
82
85
  writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
83
86
  return
84
87
  }
85
- writeJson(res, 200, { ok: true, status: 'healthy', version: pkgVersion || 'unknown' })
88
+ const accounts = await accountsView().catch(() => [])
89
+ const sources = await collectSourceChain(state, req.socket && req.socket.localPort)
90
+ const health = assessHealth({
91
+ accounts,
92
+ sources,
93
+ snapshots: defaultSnapshotStore.stats(),
94
+ webhookUrl: live().webhookUrl,
95
+ ollama: live().ollamaFallback !== false,
96
+ })
97
+ writeJson(res, 200, { ok: true, status: health.status, version: pkgVersion || 'unknown', checks: health.checks })
86
98
  }),
87
99
  }), 'dsh-subscriptions: /health')
88
100
 
@@ -310,6 +322,8 @@ export function registerStatusRoutes(ctx, state) {
310
322
  report: await diagnosticsReport(),
311
323
  conflicts: getActiveConflicts(),
312
324
  route: buildRouteReport(last, accounts),
325
+ sources: await collectSourceChain(state, req.socket && req.socket.localPort),
326
+ vaultSnapshots: defaultSnapshotStore.stats(),
313
327
  })
314
328
  }),
315
329
  }), 'dsh-subscriptions: /diagnostics')
package/lib/routes.js CHANGED
@@ -9,6 +9,7 @@ import { registerSourcesRoutes } from './routes/sources.js'
9
9
  import { registerPacksRoutes } from './routes/packs.js'
10
10
  import { registerWebhookRoutes } from './routes/webhook.js'
11
11
  import { registerVaultRoutes } from './routes/vault.js'
12
+ import { registerMetricsRoutes } from './routes/metrics.js'
12
13
  import { registerPluginUpdater } from './updater.js'
13
14
 
14
15
  export function registerRoutes(ctx, state) {
@@ -117,4 +118,5 @@ function esc(x){return String(x==null?'':x).replace(/&/g,'&amp;').replace(/</g,'
117
118
  registerPacksRoutes(ctx)
118
119
  registerWebhookRoutes(ctx, state)
119
120
  registerVaultRoutes(ctx, state)
121
+ registerMetricsRoutes(ctx, state)
120
122
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.7.9",
3
+ "version": "0.7.10",
4
4
  "description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
5
5
  "license": "MIT",
6
6
  "type": "module",