@goodandready/dsh-key-rotation 0.7.22 → 0.7.23

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.
Files changed (2) hide show
  1. package/lib/index.js +42 -11
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -92,8 +92,18 @@ const latencyHistogram = new LatencyHistogram();
92
92
  const quotaStore = new QuotaStore();
93
93
  const agentBudget = new AgentBudget();
94
94
  const regionMap = new RegionMap();
95
- const _incidentToken = "a331cc847cb47a0adf5a66b7bcbc3502cb6e7dfe";
96
- const incidentReporter = new IncidentReporter({ token: _incidentToken, baseUrl: 'http://192.168.1.111:3005', repo: 'goodandready/dsh-key-rotation', fetchImpl: globalThis.fetch });
95
+ // IncidentReporter: lazily built when Config provides incidentGitHubToken + incidentGitHubBaseUrl.
96
+ // ponytail: never bake the token into source; repo is hardcoded (this plugin's home repo) but token is per-deploy.
97
+ let incidentReporter = null;
98
+ function ensureIncidentReporter() {
99
+ if (incidentReporter) return incidentReporter;
100
+ const cfg = getConfig();
101
+ const token = cfg ? cfg.incidentGitHubToken : '';
102
+ const baseUrl = cfg ? cfg.incidentGitHubBaseUrl : '';
103
+ if (!token || !baseUrl) return null;
104
+ incidentReporter = new IncidentReporter({ token, baseUrl, repo: 'goodandready/dsh-key-rotation', fetchImpl: globalThis.fetch });
105
+ return incidentReporter;
106
+ }
97
107
  const shadowRouter = new ShadowRouter({ primary: '', secondary: '', percent: 0 });let sandboxRunner = null;
98
108
  const webhookSender = new WebhookSender({ fetchImpl: globalThis.fetch });
99
109
  function ensureSandboxRunner(ctx) {
@@ -145,6 +155,9 @@ export const Config = Schema.object({
145
155
  selfHealIdleMs: Schema.number().default(3600000),
146
156
  latencyEnabled: Schema.boolean().default(true),
147
157
  latencyWindow: Schema.number().default(200),
158
+ incidentGitHubToken: Schema.string().default(''),
159
+ incidentGitHubBaseUrl: Schema.string().default(''),
160
+ incidentThreshold: Schema.number().default(5),
148
161
  rateLimitThreshold: Schema.number().default(0.1),
149
162
  providers: Schema.array(Schema.object({
150
163
  provider: Schema.string().required(),
@@ -548,7 +561,7 @@ export function apply(ctx, config = {}) {
548
561
  for (const key of [...poolState.keys()]) {
549
562
  if (![...poolByRef.values()].some((p) => p.base === key)) poolState.delete(key);
550
563
  }
551
- return { switchCodes, cooldownMs, maxCooldownMs, notifyWebhook, notifyThreshold, backupDir, backupIntervalMs, backupKeep, rotationScheduleDays, rateLimitThreshold, poolByRef, providerToPool, modelPoolByProvider, cloneIds };
564
+ return { switchCodes, cooldownMs, maxCooldownMs, notifyWebhook, notifyThreshold, incidentThreshold, backupDir, backupIntervalMs, backupKeep, rotationScheduleDays, rateLimitThreshold, poolByRef, providerToPool, modelPoolByProvider, cloneIds };
552
565
  }
553
566
 
554
567
  // ── patch credentials.resolve: pool refs resolve to the next healthy key ──
@@ -756,14 +769,8 @@ export function apply(ctx, config = {}) {
756
769
  pool.state.lastExhaustionAt = Date.now();
757
770
  pool.state.exhaustionCount = (pool.state.exhaustionCount ?? 0) + 1;
758
771
  console.warn(`[dsh-key-rotation] ${options.provider}: pool exhausted — all ${pool.refs.length} keys cooling`);
759
- // notify webhook if configured and threshold reached
760
- try {
761
- const { notifyWebhook, notifyThreshold } = buildRuntime();
762
- if (notifyWebhook && pool.state.exhaustionCount >= notifyThreshold) {
763
- const payload = JSON.stringify({ provider: options.provider, exhaustionCount: pool.state.exhaustionCount, at: pool.state.lastExhaustionAt, keys: pool.refs });
764
- fetch(notifyWebhook, { method: 'POST', headers: { 'content-type': 'application/json' }, body: payload }).catch(()=>{});
765
- }
766
- } catch {}
772
+ // notify via extracted helper (see notifyExhaustion above)
773
+ notifyExhaustion(buildRuntime(), pool, { provider: options.provider });
767
774
 
768
775
  yield lastFailure ?? finishError('TRANSPORT', 'dsh-key-rotation: all keys failed');
769
776
  })();
@@ -1200,3 +1207,27 @@ export function apply(ctx, config = {}) {
1200
1207
  });
1201
1208
  });
1202
1209
  }
1210
+
1211
+ // Notify on exhaustion: webhook + (optional) GitHub incident.
1212
+ // Extracted at module scope for testability. No I/O outside the injected hooks.
1213
+ // ponytail: thresholds and URLs are runtime-resolved per call, so changing Config is reflected immediately.
1214
+ export function notifyExhaustion(runtime, pool, options, hooks = { webhookSender, ensureIncidentReporter }) {
1215
+ if (!runtime || !pool) return;
1216
+ const count = pool.state ? (pool.state.exhaustionCount ?? 0) : 0;
1217
+ if (count <= 0) return;
1218
+ try {
1219
+ if (runtime.notifyWebhook && count >= (runtime.notifyThreshold ?? 0)) {
1220
+ hooks.webhookSender.send(runtime.notifyWebhook, {
1221
+ provider: options.provider,
1222
+ exhaustionCount: count,
1223
+ at: pool.state.lastExhaustionAt,
1224
+ keys: pool.refs,
1225
+ });
1226
+ }
1227
+ if (runtime.incidentThreshold && count >= runtime.incidentThreshold) {
1228
+ const reporter = hooks.ensureIncidentReporter();
1229
+ if (reporter) reporter.open(options.provider, pool.state.lastExhaustionAt);
1230
+ }
1231
+ } catch (_) { /* ponytail: never crash rotate() */ }
1232
+ }
1233
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-key-rotation",
3
- "version": "0.7.22",
3
+ "version": "0.7.23",
4
4
  "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.",
5
5
  "keywords": [
6
6
  "deepseek-harness",