@goodandready/dsh-key-rotation 0.8.77 → 0.8.79
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 +14 -0
- package/lib/client-src/00-header.js +40 -0
- package/lib/client-src/10-locales.js +585 -0
- package/lib/client-src/20-hooks.js +55 -0
- package/lib/client-src/30-utils.js +185 -0
- package/lib/client-src/40-components.js +346 -0
- package/lib/client-src/50-card-main.js +647 -0
- package/lib/client-src/51-card-pool-mutators.js +501 -0
- package/lib/client-src/52-card-pool-rows.js +590 -0
- package/lib/client-src/53-card-dialogs-and-render.js +568 -0
- package/lib/client-src/60-header-chip.js +137 -0
- package/lib/client-src/90-apply.js +69 -0
- package/lib/config-compat.js +18 -1
- package/lib/http-bridge.js +2 -0
- package/lib/index.js +7 -1
- package/lib/key-state-machine.js +267 -0
- package/lib/model-catalog.js +296 -0
- package/lib/ops-balance.js +2 -147
- package/lib/ops-config-io.js +2 -456
- package/lib/ops-keys.js +2 -567
- package/lib/ops-metrics.js +2 -92
- package/lib/ops-paths.js +2 -24
- package/lib/ops-quick-action.js +2 -168
- package/lib/ops-status.js +2 -269
- package/lib/ops-telemetry.js +2 -276
- package/lib/ops-test.js +2 -255
- package/lib/ops-webhook.js +2 -218
- package/lib/pool-selection.js +36 -13
- package/lib/pool-state.js +3 -0
- package/lib/pool.js +10 -15
- package/lib/quota-runway.js +222 -0
- package/lib/routes/balance.js +147 -0
- package/lib/routes/config.js +456 -0
- package/lib/routes/guard.js +13 -0
- package/lib/routes/index.js +43 -0
- package/lib/routes/keys.js +567 -0
- package/lib/routes/metrics.js +92 -0
- package/lib/routes/paths.js +24 -0
- package/lib/routes/quick-action.js +168 -0
- package/lib/routes/status.js +269 -0
- package/lib/routes/telemetry.js +276 -0
- package/lib/routes/testing.js +255 -0
- package/lib/routes/webhook.js +218 -0
- package/lib/routes-ops.js +2 -29
- package/lib/stream-metrics.js +13 -5
- package/lib/transports/anthropic.js +125 -0
- package/lib/transports/index.js +23 -0
- package/lib/transports/openai.js +93 -0
- package/lib/transports/sse.js +160 -0
- package/lib/transports/tokens.js +124 -0
- package/package.json +15 -10
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// lib/model-catalog.js — Centralized model catalog with Cache Stampede protection (#550).
|
|
2
|
+
// Deduplicates concurrent in-flight requests and provides TTL-based caching and model capability normalization.
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_CATALOG_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
5
|
+
export const DEFAULT_CATALOG_TIMEOUT_MS = 15_000; // 15 seconds
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
8
|
+
export const DEFAULT_MAX_TOKENS = 8_192;
|
|
9
|
+
|
|
10
|
+
export const KNOWN_MODELS_FALLBACK = {
|
|
11
|
+
'deepseek-chat': {
|
|
12
|
+
id: 'deepseek-chat',
|
|
13
|
+
name: 'DeepSeek Chat',
|
|
14
|
+
contextWindow: 64_000,
|
|
15
|
+
maxTokens: 8_000,
|
|
16
|
+
supportsReasoning: false,
|
|
17
|
+
supportsTools: true,
|
|
18
|
+
supportsVision: false,
|
|
19
|
+
},
|
|
20
|
+
'deepseek-reasoner': {
|
|
21
|
+
id: 'deepseek-reasoner',
|
|
22
|
+
name: 'DeepSeek Reasoner',
|
|
23
|
+
contextWindow: 64_000,
|
|
24
|
+
maxTokens: 8_000,
|
|
25
|
+
supportsReasoning: true,
|
|
26
|
+
supportsTools: false,
|
|
27
|
+
supportsVision: false,
|
|
28
|
+
},
|
|
29
|
+
'gpt-4o': {
|
|
30
|
+
id: 'gpt-4o',
|
|
31
|
+
name: 'GPT-4o',
|
|
32
|
+
contextWindow: 128_000,
|
|
33
|
+
maxTokens: 16_384,
|
|
34
|
+
supportsReasoning: false,
|
|
35
|
+
supportsTools: true,
|
|
36
|
+
supportsVision: true,
|
|
37
|
+
},
|
|
38
|
+
'gpt-4o-mini': {
|
|
39
|
+
id: 'gpt-4o-mini',
|
|
40
|
+
name: 'GPT-4o mini',
|
|
41
|
+
contextWindow: 128_000,
|
|
42
|
+
maxTokens: 16_384,
|
|
43
|
+
supportsReasoning: false,
|
|
44
|
+
supportsTools: true,
|
|
45
|
+
supportsVision: true,
|
|
46
|
+
},
|
|
47
|
+
'claude-3-5-sonnet-20241022': {
|
|
48
|
+
id: 'claude-3-5-sonnet-20241022',
|
|
49
|
+
name: 'Claude 3.5 Sonnet',
|
|
50
|
+
contextWindow: 200_000,
|
|
51
|
+
maxTokens: 8_192,
|
|
52
|
+
supportsReasoning: false,
|
|
53
|
+
supportsTools: true,
|
|
54
|
+
supportsVision: true,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Normalizes raw model object from provider /models endpoint.
|
|
60
|
+
* @param {object|string} raw
|
|
61
|
+
* @returns {object}
|
|
62
|
+
*/
|
|
63
|
+
export function normalizeModelMeta(raw) {
|
|
64
|
+
if (typeof raw === 'string') {
|
|
65
|
+
const fallback = KNOWN_MODELS_FALLBACK[raw];
|
|
66
|
+
return fallback ? { ...fallback } : {
|
|
67
|
+
id: raw,
|
|
68
|
+
name: raw,
|
|
69
|
+
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
|
70
|
+
maxTokens: DEFAULT_MAX_TOKENS,
|
|
71
|
+
supportsReasoning: raw.includes('reason') || raw.includes('r1') || raw.includes('thought'),
|
|
72
|
+
supportsTools: true,
|
|
73
|
+
supportsVision: raw.includes('vision') || raw.includes('4o') || raw.includes('vl'),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const id = String(raw?.id || raw?.slug || raw?.name || '');
|
|
78
|
+
const fallback = KNOWN_MODELS_FALLBACK[id];
|
|
79
|
+
|
|
80
|
+
const name = String(raw?.name || raw?.display_name || id);
|
|
81
|
+
const contextWindow =
|
|
82
|
+
Number(raw?.context_window || raw?.max_context_window || raw?.contextWindow) ||
|
|
83
|
+
fallback?.contextWindow ||
|
|
84
|
+
DEFAULT_CONTEXT_WINDOW;
|
|
85
|
+
const maxTokens =
|
|
86
|
+
Number(raw?.max_tokens || raw?.max_output_tokens || raw?.maxTokens) ||
|
|
87
|
+
fallback?.maxTokens ||
|
|
88
|
+
DEFAULT_MAX_TOKENS;
|
|
89
|
+
|
|
90
|
+
const caps = Array.isArray(raw?.capabilities) ? raw.capabilities : [];
|
|
91
|
+
const reasoningLevels = Array.isArray(raw?.supported_reasoning_levels) ? raw.supported_reasoning_levels : [];
|
|
92
|
+
|
|
93
|
+
const supportsReasoning =
|
|
94
|
+
Boolean(raw?.supportsReasoning) ||
|
|
95
|
+
caps.includes('thinking') ||
|
|
96
|
+
caps.includes('reasoning') ||
|
|
97
|
+
reasoningLevels.length > 0 ||
|
|
98
|
+
id.includes('reason') ||
|
|
99
|
+
id.includes('r1') ||
|
|
100
|
+
fallback?.supportsReasoning ||
|
|
101
|
+
false;
|
|
102
|
+
|
|
103
|
+
const supportsTools =
|
|
104
|
+
raw?.supportsTools !== undefined
|
|
105
|
+
? Boolean(raw.supportsTools)
|
|
106
|
+
: caps.includes('tools') || fallback?.supportsTools || true;
|
|
107
|
+
|
|
108
|
+
const inputModalities = Array.isArray(raw?.input_modalities) ? raw.input_modalities : [];
|
|
109
|
+
const supportsVision =
|
|
110
|
+
Boolean(raw?.supportsVision) ||
|
|
111
|
+
inputModalities.includes('image') ||
|
|
112
|
+
id.includes('vision') ||
|
|
113
|
+
id.includes('4o') ||
|
|
114
|
+
id.includes('vl') ||
|
|
115
|
+
fallback?.supportsVision ||
|
|
116
|
+
false;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
id,
|
|
120
|
+
name,
|
|
121
|
+
contextWindow,
|
|
122
|
+
maxTokens,
|
|
123
|
+
supportsReasoning,
|
|
124
|
+
supportsTools,
|
|
125
|
+
supportsVision,
|
|
126
|
+
raw,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export class ModelCatalog {
|
|
131
|
+
/**
|
|
132
|
+
* @param {object} [options]
|
|
133
|
+
* @param {number} [options.ttlMs]
|
|
134
|
+
* @param {number} [options.timeoutMs]
|
|
135
|
+
* @param {typeof fetch} [options.fetchImpl]
|
|
136
|
+
*/
|
|
137
|
+
constructor(options = {}) {
|
|
138
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_CATALOG_TTL_MS;
|
|
139
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_CATALOG_TIMEOUT_MS;
|
|
140
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
141
|
+
/** @type {Map<string, { at: number, models: Array<object> }>} */
|
|
142
|
+
this.cache = new Map();
|
|
143
|
+
/** @type {Map<string, Promise<Array<object>>>} In-flight deduplication map */
|
|
144
|
+
this.inflight = new Map();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
_buildKey(endpoint, apiKey) {
|
|
148
|
+
const keyHint = apiKey ? apiKey.slice(-6) : 'anonymous';
|
|
149
|
+
return `${endpoint || 'default'}|${keyHint}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Gets list of models for an endpoint.
|
|
154
|
+
* If a request for this endpoint is already in flight, reuses the promise (anti-stampede).
|
|
155
|
+
*
|
|
156
|
+
* @param {object} options
|
|
157
|
+
* @param {string} options.endpoint Base URL or /models URL
|
|
158
|
+
* @param {string} [options.apiKey]
|
|
159
|
+
* @param {boolean} [options.forceRefresh]
|
|
160
|
+
* @param {typeof fetch} [options.fetchImpl]
|
|
161
|
+
* @returns {Promise<Array<object>>}
|
|
162
|
+
*/
|
|
163
|
+
async getModels(options = {}) {
|
|
164
|
+
const { endpoint, apiKey, forceRefresh = false } = options;
|
|
165
|
+
const fetchFn = options.fetchImpl || this.fetchImpl;
|
|
166
|
+
const cacheKey = this._buildKey(endpoint, apiKey);
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
|
|
169
|
+
// 1. Check TTL cache if not forcing refresh
|
|
170
|
+
if (!forceRefresh) {
|
|
171
|
+
const cached = this.cache.get(cacheKey);
|
|
172
|
+
if (cached && (now - cached.at < this.ttlMs)) {
|
|
173
|
+
return cached.models;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 2. Check in-flight deduplication map (Cache Stampede protection)
|
|
178
|
+
const existingPromise = this.inflight.get(cacheKey);
|
|
179
|
+
if (existingPromise) {
|
|
180
|
+
return existingPromise;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 3. Initiate single fetch and record in inflight map
|
|
184
|
+
const fetchPromise = (async () => {
|
|
185
|
+
try {
|
|
186
|
+
const models = await this._fetchModels(endpoint, apiKey, fetchFn);
|
|
187
|
+
this.cache.set(cacheKey, { at: Date.now(), models });
|
|
188
|
+
return models;
|
|
189
|
+
} catch (err) {
|
|
190
|
+
// On error, if we have stale cache, serve stale cache
|
|
191
|
+
const stale = this.cache.get(cacheKey);
|
|
192
|
+
if (stale && Array.isArray(stale.models) && stale.models.length > 0) {
|
|
193
|
+
return stale.models;
|
|
194
|
+
}
|
|
195
|
+
// Fallback to known models
|
|
196
|
+
return Object.values(KNOWN_MODELS_FALLBACK).map(normalizeModelMeta);
|
|
197
|
+
} finally {
|
|
198
|
+
this.inflight.delete(cacheKey);
|
|
199
|
+
}
|
|
200
|
+
})();
|
|
201
|
+
|
|
202
|
+
this.inflight.set(cacheKey, fetchPromise);
|
|
203
|
+
return fetchPromise;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Gets metadata for a specific model ID.
|
|
208
|
+
* @param {string} modelId
|
|
209
|
+
* @param {object} [options]
|
|
210
|
+
* @returns {Promise<object|null>}
|
|
211
|
+
*/
|
|
212
|
+
async getModel(modelId, options = {}) {
|
|
213
|
+
if (!modelId) return null;
|
|
214
|
+
const models = await this.getModels(options);
|
|
215
|
+
const found = models.find((m) => m.id === modelId);
|
|
216
|
+
if (found) return found;
|
|
217
|
+
return normalizeModelMeta(modelId);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async _fetchModels(endpoint, apiKey, fetchFn) {
|
|
221
|
+
if (!endpoint || typeof fetchFn !== 'function') {
|
|
222
|
+
return Object.values(KNOWN_MODELS_FALLBACK).map(normalizeModelMeta);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let url = endpoint;
|
|
226
|
+
if (!url.endsWith('/models') && !url.includes('/models?')) {
|
|
227
|
+
url = url.replace(/\/+$/, '') + '/models';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const headers = {
|
|
231
|
+
accept: 'application/json',
|
|
232
|
+
'user-agent': 'dsh-key-rotation/model-catalog',
|
|
233
|
+
};
|
|
234
|
+
if (apiKey) {
|
|
235
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const controller = new AbortController();
|
|
239
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const resp = await fetchFn(url, {
|
|
243
|
+
headers,
|
|
244
|
+
signal: controller.signal,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (!resp.ok) {
|
|
248
|
+
throw new Error(`Catalog fetch failed with status ${resp.status}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const body = await resp.json();
|
|
252
|
+
let rawList = [];
|
|
253
|
+
|
|
254
|
+
if (Array.isArray(body?.data)) {
|
|
255
|
+
rawList = body.data;
|
|
256
|
+
} else if (Array.isArray(body?.models)) {
|
|
257
|
+
rawList = body.models;
|
|
258
|
+
} else if (body?.models && typeof body.models === 'object') {
|
|
259
|
+
rawList = Object.entries(body.models).map(([k, v]) => ({ id: k, ...(typeof v === 'object' ? v : {}) }));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (rawList.length === 0) {
|
|
263
|
+
return Object.values(KNOWN_MODELS_FALLBACK).map(normalizeModelMeta);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return rawList.map(normalizeModelMeta);
|
|
267
|
+
} finally {
|
|
268
|
+
clearTimeout(timer);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
invalidate(endpoint, apiKey) {
|
|
273
|
+
const key = this._buildKey(endpoint, apiKey);
|
|
274
|
+
this.cache.delete(key);
|
|
275
|
+
this.inflight.delete(key);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
clear() {
|
|
279
|
+
this.cache.clear();
|
|
280
|
+
this.inflight.clear();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
getStatus(endpoint, apiKey) {
|
|
284
|
+
const key = this._buildKey(endpoint, apiKey);
|
|
285
|
+
const entry = this.cache.get(key);
|
|
286
|
+
const isInflight = this.inflight.has(key);
|
|
287
|
+
return {
|
|
288
|
+
cached: Boolean(entry),
|
|
289
|
+
count: entry?.models?.length ?? 0,
|
|
290
|
+
fetchedAt: entry?.at ?? null,
|
|
291
|
+
inflight: isInflight,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export const defaultModelCatalog = new ModelCatalog();
|
package/lib/ops-balance.js
CHANGED
|
@@ -1,147 +1,2 @@
|
|
|
1
|
-
// lib/ops-balance.js —
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
json,
|
|
5
|
-
readJson,
|
|
6
|
-
} from './http-bridge.js';
|
|
7
|
-
import {
|
|
8
|
-
isTrustedBridgeRequest,
|
|
9
|
-
isValidRef,
|
|
10
|
-
} from './pool.js';
|
|
11
|
-
import { BALANCE_PATH } from './ops-paths.js';
|
|
12
|
-
import { inspectBalanceForRef } from './balance-inspector.js';
|
|
13
|
-
import { pushEvent } from './notify-events.js';
|
|
14
|
-
|
|
15
|
-
export function registerBalanceRoutes(ctx, deps) {
|
|
16
|
-
const {
|
|
17
|
-
poolState,
|
|
18
|
-
buildRuntime,
|
|
19
|
-
} = deps;
|
|
20
|
-
|
|
21
|
-
ctx.effect(() => ctx.webServer.register({
|
|
22
|
-
kind: 'exact',
|
|
23
|
-
path: BALANCE_PATH,
|
|
24
|
-
handler: async (req, res) => {
|
|
25
|
-
if (!isTrustedBridgeRequest(req)) {
|
|
26
|
-
json(res, 403, { ok: false, error: 'Forbidden' });
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const urlObj = new URL(req.url, 'http://127.0.0.1');
|
|
31
|
-
|
|
32
|
-
// GET: Query single key or provider cached balances
|
|
33
|
-
if (req.method === 'GET') {
|
|
34
|
-
const ref = urlObj.searchParams.get('ref');
|
|
35
|
-
const provider = urlObj.searchParams.get('provider');
|
|
36
|
-
|
|
37
|
-
if (ref) {
|
|
38
|
-
const runtime = buildRuntime();
|
|
39
|
-
const targetPool = provider ? runtime.pools?.find((p) => p.provider === provider) : null;
|
|
40
|
-
const pool = targetPool ?? runtime.poolByRef?.get(ref);
|
|
41
|
-
const cached = pool?.state?.balances?.get(ref) ?? null;
|
|
42
|
-
|
|
43
|
-
if (urlObj.searchParams.get('refresh') === 'true' && provider) {
|
|
44
|
-
const inspectRes = await inspectBalanceForRef(ctx, ref, provider);
|
|
45
|
-
if (inspectRes.ok) {
|
|
46
|
-
if (pool && pool.state) {
|
|
47
|
-
if (!pool.state.balances) pool.state.balances = new Map();
|
|
48
|
-
pool.state.balances.set(ref, { ...inspectRes, at: Date.now() });
|
|
49
|
-
|
|
50
|
-
// Check soft pause
|
|
51
|
-
applyBalanceState(pool, ref, inspectRes);
|
|
52
|
-
}
|
|
53
|
-
json(res, 200, { ok: true, ref, balance: inspectRes });
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
json(res, 200, { ok: false, ref, error: inspectRes.error, cached });
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
json(res, 200, { ok: true, ref, balance: cached });
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Return all cached balances
|
|
65
|
-
const out = {};
|
|
66
|
-
if (poolState && typeof poolState.forEach === 'function') {
|
|
67
|
-
poolState.forEach((st, provName) => {
|
|
68
|
-
if (st && st.balances instanceof Map) {
|
|
69
|
-
for (const [k, v] of st.balances) {
|
|
70
|
-
out[k] = v;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
json(res, 200, { ok: true, balances: out });
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// POST: Trigger refresh of balances for a provider or key list
|
|
80
|
-
if (req.method === 'POST') {
|
|
81
|
-
const body = await readJson(req);
|
|
82
|
-
const provider = body?.provider;
|
|
83
|
-
const ref = body?.ref;
|
|
84
|
-
const testValue = body?.value;
|
|
85
|
-
|
|
86
|
-
if (ref && provider) {
|
|
87
|
-
const inspectRes = await inspectBalanceForRef(ctx, ref, provider, { testValue });
|
|
88
|
-
const runtime = buildRuntime();
|
|
89
|
-
const pool = runtime.pools?.find((p) => p.provider === provider) ?? runtime.poolByRef?.get(ref);
|
|
90
|
-
|
|
91
|
-
if (inspectRes.ok && pool && pool.state) {
|
|
92
|
-
if (!pool.state.balances) pool.state.balances = new Map();
|
|
93
|
-
pool.state.balances.set(ref, { ...inspectRes, at: Date.now() });
|
|
94
|
-
applyBalanceState(pool, ref, inspectRes);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
json(res, inspectRes.ok ? 200 : 400, { ok: inspectRes.ok, ref, balance: inspectRes });
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (provider) {
|
|
102
|
-
const runtime = buildRuntime();
|
|
103
|
-
const pool = runtime.pools?.find((p) => p.provider === provider);
|
|
104
|
-
if (!pool || !Array.isArray(pool.refs) || pool.refs.length === 0) {
|
|
105
|
-
json(res, 404, { ok: false, error: `Provider '${provider}' not found or has no keys` });
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const results = {};
|
|
110
|
-
for (const kRef of pool.refs) {
|
|
111
|
-
const r = await inspectBalanceForRef(ctx, kRef, provider);
|
|
112
|
-
results[kRef] = r;
|
|
113
|
-
if (r.ok && pool.state) {
|
|
114
|
-
if (!pool.state.balances) pool.state.balances = new Map();
|
|
115
|
-
pool.state.balances.set(kRef, { ...r, at: Date.now() });
|
|
116
|
-
applyBalanceState(pool, kRef, r);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
json(res, 200, { ok: true, provider, balances: results });
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
json(res, 400, { ok: false, error: 'ref or provider required' });
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
json(res, 405, { ok: false, error: 'Method Not Allowed' });
|
|
128
|
-
},
|
|
129
|
-
}), 'dsh-key-rotation: balance routes');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function applyBalanceState(pool, ref, balanceRes) {
|
|
133
|
-
if (!pool || !pool.state) return;
|
|
134
|
-
const isZero = balanceRes.isAvailable === false || (typeof balanceRes.total === 'number' && balanceRes.total <= 0);
|
|
135
|
-
|
|
136
|
-
if (isZero) {
|
|
137
|
-
if (!pool.state.pausedRefs) pool.state.pausedRefs = new Set();
|
|
138
|
-
if (pool.state.pausedRefs instanceof Set) pool.state.pausedRefs.add(ref);
|
|
139
|
-
if (!pool.state.pauseReasons) pool.state.pauseReasons = new Map();
|
|
140
|
-
pool.state.pauseReasons.set(ref, 'EMPTY_BALANCE_PAUSED');
|
|
141
|
-
pushEvent(pool, ref, 'BALANCE_EMPTY', 0);
|
|
142
|
-
} else if (pool.state.pauseReasons?.get?.(ref) === 'EMPTY_BALANCE_PAUSED') {
|
|
143
|
-
if (pool.state.pausedRefs instanceof Set) pool.state.pausedRefs.delete(ref);
|
|
144
|
-
pool.state.pauseReasons.delete(ref);
|
|
145
|
-
pushEvent(pool, ref, 'BALANCE_RESTORED', 0);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
1
|
+
// lib/ops-balance.js — Re-export facade for lib/routes/ (#522).
|
|
2
|
+
export * from "./routes/balance.js";
|