@goodandready/dsh-key-rotation 0.8.78 → 0.8.80

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/lib/index.js CHANGED
@@ -11,7 +11,8 @@ import { decryptSecret } from './crypto-storage.js';
11
11
 
12
12
  import { AsyncLocalStorage } from 'node:async_hooks';
13
13
  import Schema from '@deepseek-ai/schemastery';
14
- import { createConfigReader, createProviderProfilesReader } from './config-compat.js';
14
+ import { createConfigReader, createProviderProfilesReader, unwrapVolatileConfig } from './config-compat.js';
15
+ import { KeyStateMachine, KEY_STATES, VALID_TRANSITIONS } from './key-state-machine.js';
15
16
  import {
16
17
  keyTail, KEY_TAIL_CHARS, KEY_TAIL_PLACEHOLDER, isLoopbackAddress, isTrustedBridgeRequest, isDesktopShellRequest, SWITCHABLE_MESSAGE_PATTERN,
17
18
  DEFAULT_SWITCH_CODES, isValidRef, pickNext, applyCooldown, recordFailure,
@@ -54,15 +55,18 @@ import { LOCAL_QUOTA_CODE } from './rotate.js';
54
55
  import { setupIdleHealEffect, setupAutoUnbreakEffect, setupPersistence, setupProactiveHealthProbeEffect, setupKeyDrainEffect } from './lifecycle.js';
55
56
 
56
57
  export const name = '@goodandready/dsh-key-rotation';
57
- export const inject = ['llm', 'webServer', 'settings', 'credentials'];
58
+ export const inject = ['llm'];
58
59
  export { keyTail, KEY_TAIL_CHARS, KEY_TAIL_PLACEHOLDER, isLoopbackAddress, isTrustedBridgeRequest, isDesktopShellRequest, DEFAULT_SWITCH_CODES, isSwitchableError, formatExhaustionMessage, getRuntime };
59
60
  export { notifySwitch, notifyExhaustion, notifyEarlyWarning };
60
61
  export { classifyResponseError } from './errors.js';
61
62
  export { SessionRouter, defaultSessionRouter, unstickFailedKey } from './session-router.js';
62
63
  export { isContentChunk } from './rotate.js';
63
64
  export { ModelCatalog, defaultModelCatalog } from './model-catalog.js';
65
+ export { KeyStateMachine, KEY_STATES, VALID_TRANSITIONS } from './key-state-machine.js';
66
+ export { unwrapVolatileConfig } from './config-compat.js';
64
67
  export { QuotaRunway, defaultQuotaRunway, estimateRunway } from './quota-runway.js';
65
68
  export * as transports from './transports/index.js';
69
+ export * as core from './core/index.js';
66
70
 
67
71
  const NS = 'dsh-key-rotation';
68
72
 
@@ -221,7 +225,6 @@ export function apply(ctx, config = {}) {
221
225
  const { ensureSandboxRunner, probeRef, lastTestCache } = sandboxService;
222
226
 
223
227
  const readProfiles = createProviderProfilesReader(ctx, PIAI_NS);
224
- registerConfigBridge(ctx, () => buildRuntime().cloneIds);
225
228
  ensureSandboxRunner(ctx);
226
229
 
227
230
  setupIdleHealEffect(ctx, getConfig, buildRuntime);
@@ -243,6 +246,40 @@ export function apply(ctx, config = {}) {
243
246
 
244
247
  setupAutoUnbreakEffect(ctx, getConfig, buildRuntime, ensureSandboxRunner, logger);
245
248
 
249
+ // Dynamic webServer registration (#528)
250
+ const setupWebRoutes = (webCtx = ctx) => {
251
+ const target = webCtx?.webServer ? webCtx : ctx;
252
+ registerConfigBridge(target, () => buildRuntime().cloneIds);
253
+ registerOpsRoutes(target, {
254
+ buildRuntime,
255
+ latencyHistogram,
256
+ lastTestCache,
257
+ ensureSandboxRunner,
258
+ poolState,
259
+ getRotationDisabled: () => rotationDisabled,
260
+ setRotationDisabled: (v) => { rotationDisabled = v; },
261
+ circuitBreaker: moduleBreaker,
262
+ notifyQueue: moduleNotifyQueue,
263
+ quotaStore,
264
+ keyDrainManager,
265
+ selectionTraceStore,
266
+ diagBuffer,
267
+ });
268
+ if (typeof target.webServer?.register === 'function') {
269
+ registerPluginUpdater(target, {
270
+ endpoint: '/api/dsh-key-rotation/update',
271
+ packageName: '@goodandready/dsh-key-rotation',
272
+ manifestUrl: new URL('../package.json', import.meta.url),
273
+ });
274
+ }
275
+ };
276
+
277
+ if (ctx.webServer) {
278
+ setupWebRoutes(ctx);
279
+ } else if (typeof ctx.inject === 'function') {
280
+ ctx.inject(['webServer'], setupWebRoutes);
281
+ }
282
+
246
283
  // Periodic sweep of expired cooldowns
247
284
  ctx.effect(() => {
248
285
  const id = setInterval(() => {
@@ -468,30 +505,7 @@ export function apply(ctx, config = {}) {
468
505
  logger,
469
506
  });
470
507
 
471
- registerOpsRoutes(ctx, {
472
- buildRuntime,
473
- latencyHistogram,
474
- lastTestCache,
475
- ensureSandboxRunner,
476
- poolState,
477
- getRotationDisabled: () => rotationDisabled,
478
- setRotationDisabled: (v) => { rotationDisabled = v; },
479
- circuitBreaker: moduleBreaker,
480
- notifyQueue: moduleNotifyQueue,
481
- quotaStore,
482
- keyDrainManager,
483
- selectionTraceStore,
484
- diagBuffer,
485
- });
486
-
487
- ctx.effect(() => {
488
- if (typeof ctx.webServer?.register !== 'function') return;
489
- registerPluginUpdater(ctx, {
490
- endpoint: '/api/dsh-key-rotation/update',
491
- packageName: '@goodandready/dsh-key-rotation',
492
- manifestUrl: new URL('../package.json', import.meta.url),
493
- });
494
- }, 'dsh-key-rotation: plugin updater');
508
+ // routes registered dynamically above via setupWebRoutes
495
509
 
496
510
  ctx.effect(() => ctx.on('llm/stream', (options, next) => {
497
511
  if (options[MARKER]) return next();
@@ -0,0 +1,2 @@
1
+ // lib/key-state-machine.js — Backwards-compatible facade (#530).
2
+ export * from './core/state-machine.js';
@@ -1,147 +1,2 @@
1
- // lib/ops-balance.js — operational HTTP routes for balance inspection (#495).
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";