@goodandready/dsh-key-rotation 0.8.82 → 0.8.84

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.83 (2026-10-31)
4
+
5
+ ### TypeScript Migration & Type-Safe Core Architecture (Block 11 Release)
6
+ - **TypeScript Server-Side Architecture (Issue #524)**: Migrated server-side codebase to TypeScript with `src/` directory structure, strict `tsconfig.json`, and `tsdown` build pipeline generating `lib/index.d.ts`.
7
+ - **Domain Type Definitions (`src/types/`)**: Defined comprehensive TypeScript types for configuration schemas (`PoolConfig`, `ModelQuotaConfig`, `FailoverConfig`, `PluginConfig`), pool & key state machines (`KeyState`, `KeyStatus`, `PoolItem`, `ResolvedKey`, `RoutingDecision`, `QuotaWindow`), and Cordis service interaction contracts (`Context`, `LlmService`, `WebServerService`, `SettingsService`, `CredentialsService`).
8
+ - **Static Type Safety & Tooling**: Integrated `npm run typecheck` (`tsc --noEmit`) into `npm run check`, guaranteeing compile-time type safety across all configuration fields, service bindings, and event handlers with 0 errors.
9
+ - **Zero Runtime Dependencies**: All TypeScript build tools (`typescript`, `tsdown`, `@types/node`) maintained strictly in `devDependencies`, with `dependencies: {}` preserved.
10
+
3
11
  ## 0.8.82 (2026-10-30)
4
12
 
5
13
  ### Modernized UI Builder & JSX Pipeline (Block 10 Release)
package/lib/index.js CHANGED
@@ -36,6 +36,7 @@ import { CircuitBreaker } from './circuit-breaker.js';
36
36
  import { NotifyQueue } from './notify-queue.js';
37
37
  import { classifyFailure } from './error-taxonomy.js';
38
38
  import { registerOpsRoutes } from './routes-ops.js';
39
+ import { getWebServer } from './routes/guard.js';
39
40
  import { registerPluginUpdater } from './plugin-updater.js';
40
41
  import { notifySwitch, notifyExhaustion, notifyEarlyWarning, pushEvent } from './notify-events.js';
41
42
  import { QUOTA_WINDOW_TYPES } from './quota-window.js';
@@ -178,7 +179,7 @@ export const Config = typeof Schema.number().volatile === 'function'
178
179
  : SettingsSchema;
179
180
 
180
181
  function registerConfigBridge(ctx, getCloneIds) {
181
- return ctx.webServer.register({
182
+ return getWebServer(ctx).register({
182
183
  kind: 'exact',
183
184
  path: CONFIG_PATH,
184
185
  handler: (req, res) => {
@@ -246,9 +247,11 @@ export function apply(ctx, config = {}) {
246
247
 
247
248
  setupAutoUnbreakEffect(ctx, getConfig, buildRuntime, ensureSandboxRunner, logger);
248
249
 
249
- // Dynamic webServer registration (#528)
250
+ // Dynamic webServer registration (#528, #567)
250
251
  const setupWebRoutes = (webCtx = ctx) => {
251
- const target = webCtx?.webServer ? webCtx : ctx;
252
+ const target = getWebServer(webCtx) ? webCtx : ctx;
253
+ const ws = getWebServer(target);
254
+ if (!ws) return;
252
255
  registerConfigBridge(target, () => buildRuntime().cloneIds);
253
256
  registerOpsRoutes(target, {
254
257
  buildRuntime,
@@ -265,7 +268,7 @@ export function apply(ctx, config = {}) {
265
268
  selectionTraceStore,
266
269
  diagBuffer,
267
270
  });
268
- if (typeof target.webServer?.register === 'function') {
271
+ if (typeof ws?.register === 'function') {
269
272
  registerPluginUpdater(target, {
270
273
  endpoint: '/api/dsh-key-rotation/update',
271
274
  packageName: '@goodandready/dsh-key-rotation',
@@ -274,10 +277,12 @@ export function apply(ctx, config = {}) {
274
277
  }
275
278
  };
276
279
 
277
- if (ctx.webServer) {
280
+ if (Object.prototype.hasOwnProperty.call(ctx, 'webServer') && ctx.webServer) {
278
281
  setupWebRoutes(ctx);
279
282
  } else if (typeof ctx.inject === 'function') {
280
283
  ctx.inject(['webServer'], setupWebRoutes);
284
+ } else if (getWebServer(ctx)) {
285
+ setupWebRoutes(ctx);
281
286
  }
282
287
 
283
288
  // Periodic sweep of expired cooldowns
package/lib/lifecycle.js CHANGED
@@ -45,7 +45,7 @@ export function setupAutoUnbreakEffect(ctx, getConfig, buildRuntime, ensureSandb
45
45
  await autoUnbreakBrokenKeys(pools, async (ref) => {
46
46
  // A rotating resolver can substitute a healthy neighbor for this
47
47
  // quarantined ref, falsely proving that the broken key recovered.
48
- const credentials = ctx.get?.('credentials') ?? ctx.credentials;
48
+ const credentials = (typeof ctx?.get === 'function' ? ctx.get('credentials') : undefined) ?? (ctx && Object.prototype.hasOwnProperty.call(ctx, 'credentials') ? ctx.credentials : undefined);
49
49
  const resolve = credentials?.__dshKeyRotationOriginalResolve ?? credentials?.resolve;
50
50
  const hit = await resolve?.call(credentials, ref);
51
51
  const raw = typeof hit?.value === 'string' && hit.value.length > 0
@@ -197,7 +197,7 @@ export function setupProactiveHealthProbeEffect(ctx, getConfig, buildRuntime, en
197
197
  for (const ref of candidates) {
198
198
  if (disposed) break;
199
199
  try {
200
- const credentials = ctx.get?.('credentials') ?? ctx.credentials;
200
+ const credentials = (typeof ctx?.get === 'function' ? ctx.get('credentials') : undefined) ?? (ctx && Object.prototype.hasOwnProperty.call(ctx, 'credentials') ? ctx.credentials : undefined);
201
201
  const resolve = credentials?.__dshKeyRotationOriginalResolve ?? credentials?.resolve;
202
202
  const hit = await resolve?.call(credentials, ref);
203
203
  const raw = typeof hit?.value === 'string' && hit.value.length > 0 ? hit.value : envValue(ref);
@@ -314,7 +314,10 @@ function json(response, statusCode, value) {
314
314
 
315
315
  export function registerPluginUpdater(ctx, options) {
316
316
  let installing = false
317
- return ctx.webServer.register({
317
+ const viaOwn = ctx && Object.prototype.hasOwnProperty.call(ctx, 'webServer') && typeof ctx.webServer?.register === 'function' ? ctx.webServer : undefined
318
+ const viaGet = typeof ctx?.get === 'function' ? ctx.get('webServer') : undefined
319
+ const ws = viaOwn ?? (typeof viaGet?.register === 'function' ? viaGet : undefined) ?? ctx?.webServer
320
+ return ws.register({
318
321
  kind: 'exact',
319
322
  path: options.endpoint,
320
323
  handler: async (request, response) => {
@@ -1,3 +1,4 @@
1
+ import { getWebServer } from './guard.js';
1
2
  // lib/ops-balance.js — operational HTTP routes for balance inspection (#495).
2
3
 
3
4
  import {
@@ -18,7 +19,7 @@ export function registerBalanceRoutes(ctx, deps) {
18
19
  buildRuntime,
19
20
  } = deps;
20
21
 
21
- ctx.effect(() => ctx.webServer.register({
22
+ ctx.effect(() => getWebServer(ctx).register({
22
23
  kind: 'exact',
23
24
  path: BALANCE_PATH,
24
25
  handler: async (req, res) => {
@@ -1,3 +1,4 @@
1
+ import { getWebServer } from './guard.js';
1
2
  // lib/ops-config-io.js — full configuration export and import (#504).
2
3
  import crypto from 'node:crypto';
3
4
  import { readFileSync } from 'node:fs';
@@ -187,7 +188,7 @@ export function diffConfigs(currentConfig, incomingConfig, mode = 'merge') {
187
188
 
188
189
  export function registerConfigIoRoutes(ctx, deps) {
189
190
  // ── GET & POST /dsh-key-rotation/export (#552: support POST for safe passphrase transmission) ──
190
- ctx.effect(() => ctx.webServer.register({
191
+ ctx.effect(() => getWebServer(ctx).register({
191
192
  kind: 'exact',
192
193
  path: CONFIG_EXPORT_PATH,
193
194
  handler: async (req, res) => {
@@ -448,7 +449,7 @@ export function registerConfigIoRoutes(ctx, deps) {
448
449
  });
449
450
  };
450
451
 
451
- ctx.effect(() => ctx.webServer.register({
452
+ ctx.effect(() => getWebServer(ctx).register({
452
453
  kind: 'exact',
453
454
  path: CONFIG_IMPORT_PATH,
454
455
  handler: handleImport,
@@ -11,3 +11,26 @@ export {
11
11
  json,
12
12
  readJson,
13
13
  } from '../http-bridge.js';
14
+
15
+ /**
16
+ * Safely resolve webServer from a Cordis context or test mock without
17
+ * triggering Cordis's Proxy trap (`cannot get property "webServer" without inject`)
18
+ * when `webServer` is provided by a sibling plugin and not in static `inject`.
19
+ */
20
+ export function getWebServer(ctx) {
21
+ if (!ctx) return undefined;
22
+ if (Object.prototype.hasOwnProperty.call(ctx, 'webServer') && typeof ctx.webServer?.register === 'function') {
23
+ return ctx.webServer;
24
+ }
25
+ if (typeof ctx.get === 'function') {
26
+ const viaGet = ctx.get('webServer');
27
+ if (typeof viaGet?.register === 'function') return viaGet;
28
+ }
29
+ try {
30
+ const ws = ctx.webServer;
31
+ if (typeof ws?.register === 'function') return ws;
32
+ } catch {
33
+ return undefined;
34
+ }
35
+ return undefined;
36
+ }
@@ -1,3 +1,4 @@
1
+ import { getWebServer } from './guard.js';
1
2
  import { decryptSecret, encryptSecret } from '../crypto-storage.js';
2
3
  // lib/ops-keys.js — key store + cooldown reset + import routes (#312 split from routes-ops.js).
3
4
  import {
@@ -156,7 +157,7 @@ export async function discoverUnconfiguredKeys(ctx, runtimePools = []) {
156
157
 
157
158
  export function registerKeyRoutes(ctx, deps) {
158
159
 
159
- ctx.effect(() => ctx.webServer.register({
160
+ ctx.effect(() => getWebServer(ctx).register({
160
161
  kind: 'exact',
161
162
  path: DRAIN_PATH,
162
163
  handler: async (req, res) => {
@@ -219,7 +220,7 @@ ctx.effect(() => ctx.webServer.register({
219
220
  // it is never sent back, only its last few characters are (see the status
220
221
  // route) — and the route is loopback- and same-origin-gated like the config
221
222
  // bridge next to it.
222
- ctx.effect(() => ctx.webServer.register({
223
+ ctx.effect(() => getWebServer(ctx).register({
223
224
  kind: 'exact',
224
225
  path: KEY_PATH,
225
226
  handler: async (req, res) => {
@@ -289,7 +290,7 @@ ctx.effect(() => ctx.webServer.register({
289
290
 
290
291
 
291
292
  // ── reset route: clear cooldown for a provider (or a single ref) ──
292
- ctx.effect(() => ctx.webServer.register({
293
+ ctx.effect(() => getWebServer(ctx).register({
293
294
  kind: 'exact',
294
295
  path: RESET_PATH,
295
296
  handler: async (req, res) => {
@@ -374,7 +375,7 @@ ctx.effect(() => ctx.webServer.register({
374
375
  }), 'dsh-key-rotation: reset route');
375
376
 
376
377
 
377
- ctx.effect(() => ctx.webServer.register({
378
+ ctx.effect(() => getWebServer(ctx).register({
378
379
  kind: 'exact',
379
380
  path: IMPORT_PATH,
380
381
  handler: async (req, res) => {
@@ -411,7 +412,7 @@ ctx.effect(() => ctx.webServer.register({
411
412
  // Health for external panels (Beszel/Uptime)
412
413
 
413
414
  // ── discover unconfigured keys route (#491) ──
414
- ctx.effect(() => ctx.webServer.register({
415
+ ctx.effect(() => getWebServer(ctx).register({
415
416
  kind: 'exact',
416
417
  path: DISCOVER_PATH,
417
418
  handler: async (req, res) => {
@@ -434,7 +435,7 @@ ctx.effect(() => ctx.webServer.register({
434
435
  }), 'dsh-key-rotation: discover route');
435
436
 
436
437
  // ── auto-import discovered keys into pools (#491) ──
437
- ctx.effect(() => ctx.webServer.register({
438
+ ctx.effect(() => getWebServer(ctx).register({
438
439
  kind: 'exact',
439
440
  path: AUTO_IMPORT_PATH,
440
441
  handler: async (req, res) => {
@@ -528,7 +529,7 @@ ctx.effect(() => ctx.webServer.register({
528
529
  },
529
530
  }), 'dsh-key-rotation: auto-import route');
530
531
 
531
- ctx.effect(() => ctx.webServer.register({
532
+ ctx.effect(() => getWebServer(ctx).register({
532
533
  kind: 'exact',
533
534
  path: PRESET_LIMITS_PATH,
534
535
  handler: async (req, res) => {
@@ -1,3 +1,4 @@
1
+ import { getWebServer } from './guard.js';
1
2
  // lib/ops-metrics.js — Prometheus / OpenMetrics export route (#373).
2
3
  import { METRICS_PATH } from './paths.js';
3
4
  import { isTrustedBridgeRequest, isKeyPaused, isKeyRevoked } from '../pool.js';
@@ -10,7 +11,7 @@ import { hasModelQuotaConfig, isModelQuotaAvailable } from '../model-quota.js';
10
11
  export function registerMetricsRoutes(ctx, deps) {
11
12
  const { buildRuntime, poolState } = deps;
12
13
 
13
- ctx.effect(() => ctx.webServer.register({
14
+ ctx.effect(() => getWebServer(ctx).register({
14
15
  kind: 'exact',
15
16
  path: METRICS_PATH,
16
17
  handler: async (req, res) => {
@@ -1,3 +1,4 @@
1
+ import { getWebServer } from './guard.js';
1
2
  import { json, readJson, isTrustedBridgeRequest } from '../http-bridge.js';
2
3
  import { QUICK_ACTION_PATH } from './paths.js';
3
4
  import { keyTail, isKeyPaused } from '../pool.js';
@@ -9,7 +10,7 @@ import { keyTail, isKeyPaused } from '../pool.js';
9
10
  export function registerQuickActionRoutes(ctx, deps) {
10
11
  const { buildRuntime, pushEvent, circuitBreaker } = deps;
11
12
 
12
- ctx.effect(() => ctx.webServer.register({
13
+ ctx.effect(() => getWebServer(ctx).register({
13
14
  kind: 'exact',
14
15
  path: QUICK_ACTION_PATH,
15
16
  handler: async (req, res) => {
@@ -1,3 +1,4 @@
1
+ import { getWebServer } from './guard.js';
1
2
  import { getModelQuotaStatus, isModelQuotaAvailable, hasModelQuotaConfig } from '../model-quota.js';
2
3
  import { providerOfBase, basePoolForBase, modelPoolsForRef } from '../pool-index.js';
3
4
  import { decryptSecret } from '../crypto-storage.js';
@@ -44,7 +45,7 @@ export function registerStatusRoutes(ctx, deps) {
44
45
  // (a typo is otherwise silent), and how often rotation has fired.
45
46
  //
46
47
  // Key VALUES never leave the host — only the boolean fact that one exists.
47
- ctx.effect(() => ctx.webServer.register({
48
+ ctx.effect(() => getWebServer(ctx).register({
48
49
  kind: 'exact',
49
50
  path: STATUS_PATH,
50
51
  handler: async (req, res) => {
@@ -204,7 +205,7 @@ ctx.effect(() => ctx.webServer.register({
204
205
  // #209: usage report - per-key requests/cost over the last N days.
205
206
  // ?format=csv returns text/csv; ?days=N window (1..90, default 7).
206
207
 
207
- ctx.effect(() => ctx.webServer.register({
208
+ ctx.effect(() => getWebServer(ctx).register({
208
209
  kind: 'exact',
209
210
  path: HEALTH_PATH,
210
211
  handler: async (req, res) => {
@@ -1,3 +1,4 @@
1
+ import { getWebServer } from './guard.js';
1
2
  // lib/ops-telemetry.js — usage + snapshot operational routes (#312 split from routes-ops.js).
2
3
  import {
3
4
  json,
@@ -71,7 +72,7 @@ export function registerTelemetryRoutes(ctx, deps) {
71
72
  buildRuntime,
72
73
  } = deps;
73
74
 
74
- ctx.effect(() => ctx.webServer.register({
75
+ ctx.effect(() => getWebServer(ctx).register({
75
76
  kind: 'exact',
76
77
  path: USAGE_PATH,
77
78
  handler: (req, res) => {
@@ -116,7 +117,7 @@ ctx.effect(() => ctx.webServer.register({
116
117
  // Secret values never travel: only credential/env names. Token fields are
117
118
  // exported as empty strings; on import they keep existing values when empty.
118
119
 
119
- ctx.effect(() => ctx.webServer.register({
120
+ ctx.effect(() => getWebServer(ctx).register({
120
121
  kind: 'exact',
121
122
  path: SNAPSHOT_PATH,
122
123
  handler: async (req, res) => {
@@ -162,7 +163,7 @@ ctx.effect(() => ctx.webServer.register({
162
163
  }), 'dsh-key-rotation: snapshot route');
163
164
 
164
165
 
165
- ctx.effect(() => ctx.webServer.register({
166
+ ctx.effect(() => getWebServer(ctx).register({
166
167
  kind: 'exact',
167
168
  path: TRACE_PATH,
168
169
  handler: (req, res) => {
@@ -187,7 +188,7 @@ ctx.effect(() => ctx.webServer.register({
187
188
  }), 'dsh-key-rotation: trace route');
188
189
 
189
190
 
190
- ctx.effect(() => ctx.webServer.register({
191
+ ctx.effect(() => getWebServer(ctx).register({
191
192
  kind: 'exact',
192
193
  path: SIMULATE_PATH,
193
194
  handler: async (req, res) => {
@@ -243,7 +244,7 @@ ctx.effect(() => ctx.webServer.register({
243
244
  },
244
245
  }), 'dsh-key-rotation: simulate route');
245
246
 
246
- ctx.effect(() => ctx.webServer.register({
247
+ ctx.effect(() => getWebServer(ctx).register({
247
248
  kind: 'exact',
248
249
  path: DIAG_PATH,
249
250
  handler: async (req, res) => {
@@ -1,3 +1,4 @@
1
+ import { getWebServer } from './guard.js';
1
2
  // lib/ops-test.js — sandbox test + sandbox cache routes (#312 split from routes-ops.js).
2
3
  import {
3
4
  json,
@@ -29,7 +30,7 @@ export function registerTestRoutes(ctx, deps) {
29
30
  } = deps;
30
31
 
31
32
  // ── test route: dry-run a single key without rotation ──
32
- ctx.effect(() => ctx.webServer.register({
33
+ ctx.effect(() => getWebServer(ctx).register({
33
34
  kind: 'exact',
34
35
  path: TEST_PATH,
35
36
  handler: async (req, res) => {
@@ -105,7 +106,7 @@ ctx.effect(() => ctx.webServer.register({
105
106
  // straight through.
106
107
  // Read-only cache snapshot for clients (badge polling).
107
108
 
108
- ctx.effect(() => ctx.webServer.register({
109
+ ctx.effect(() => getWebServer(ctx).register({
109
110
  kind: 'exact',
110
111
  path: SANDBOX_CACHE_PATH,
111
112
  handler: (req, res) => {
@@ -125,7 +126,7 @@ ctx.effect(() => ctx.webServer.register({
125
126
  // cannot be same-origin, so a shared secret is the gate).
126
127
 
127
128
  // ── test matrix route: batch probe all configured keys (#490) ──
128
- ctx.effect(() => ctx.webServer.register({
129
+ ctx.effect(() => getWebServer(ctx).register({
129
130
  kind: 'exact',
130
131
  path: TEST_MATRIX_PATH,
131
132
  handler: async (req, res) => {
@@ -1,3 +1,4 @@
1
+ import { getWebServer } from './guard.js';
1
2
  import crypto from 'node:crypto';
2
3
  // lib/ops-webhook.js — webhook interactive action route (#312 split from routes-ops.js, #388 telegram secret-token & answerCallbackQuery).
3
4
  import {
@@ -36,7 +37,7 @@ export function registerWebhookActionRoute(ctx, deps) {
36
37
  circuitBreaker,
37
38
  } = deps;
38
39
 
39
- ctx.effect(() => ctx.webServer.register({
40
+ ctx.effect(() => getWebServer(ctx).register({
40
41
  kind: 'exact',
41
42
  path: WEBHOOK_ACTION_PATH,
42
43
  handler: async (req, res) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-key-rotation",
3
- "version": "0.8.82",
3
+ "version": "0.8.84",
4
4
  "packageManager": "pnpm@10.33.2",
5
5
  "description": "Per-provider API key rotation for DeepSeek Harness: a key pool per provider, auto-created clone routes, and switching to the next key on quota/rate-limit errors. Includes a Settings section (Key Rotation) to edit the key pools, cooldown and switch codes.",
6
6
  "keywords": [
@@ -24,7 +24,10 @@
24
24
  "type": "module",
25
25
  "main": "lib/index.js",
26
26
  "exports": {
27
- ".": "./lib/index.js",
27
+ ".": {
28
+ "types": "./lib/index.d.ts",
29
+ "default": "./lib/index.js"
30
+ },
28
31
  "./client": "./lib/client.js",
29
32
  "./transports": "./lib/transports/index.js",
30
33
  "./catalog": "./lib/model-catalog.js",
@@ -60,16 +63,19 @@
60
63
  "node": ">=18"
61
64
  },
62
65
  "peerDependencies": {
63
- "@deepseek-ai/cordis": "^4.0.1",
64
- "@deepseek-ai/dsh-llm": "^0.1.7-rc.2 || >=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-alpha.1 <0.2.2-0",
65
- "@deepseek-ai/schemastery": "^3.18.4"
66
+ "@deepseek-ai/cordis": "^4.0.1 || >=4.0.6-alpha.1 <5.0.0-0",
67
+ "@deepseek-ai/dsh-llm": "^0.1.7-rc.2 || >=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-alpha.1 <0.2.2-0 || >=0.2.2-alpha.1 <0.2.3-0",
68
+ "@deepseek-ai/schemastery": "^3.18.4 || >=3.18.6-alpha.1 <4.0.0-0"
66
69
  },
67
70
  "scripts": {
68
71
  "test": "node tools/test.mjs",
69
72
  "test:core": "node tools/test.mjs --core",
70
- "check": "node tools/check.mjs",
73
+ "check": "npm run typecheck && node tools/check.mjs",
71
74
  "build:client": "node scripts/build-client.mjs",
72
- "pretest": "node scripts/build-client.mjs"
75
+ "pretest": "node scripts/build-client.mjs",
76
+ "typecheck": "tsc --noEmit",
77
+ "build:server": "tsdown",
78
+ "build": "npm run build:server && npm run build:client"
73
79
  },
74
80
  "devDependencies": {
75
81
  "react": "18.3.1",
@@ -78,7 +84,11 @@
78
84
  "@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
79
85
  "@deepseek-ai/schemastery": "^3.18.4",
80
86
  "semver": "^7.6.3",
81
- "esbuild": "^0.28.2"
87
+ "esbuild": "^0.28.2",
88
+ "typescript": "^5.7.2",
89
+ "tsdown": "^0.22.14",
90
+ "@types/node": "^22.0.0"
82
91
  },
83
- "dependencies": {}
92
+ "dependencies": {},
93
+ "types": "./lib/index.d.ts"
84
94
  }