@goodandready/dsh-key-rotation 0.7.39 → 0.7.40

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/README.md CHANGED
@@ -57,7 +57,7 @@ Unlike naive routing proxies that alter provider identifiers, `dsh-key-rotation`
57
57
  * **The provider identity never changes**: Agent replay states, multi-call turns, and tool schemas remain 100% consistent.
58
58
  * **Pre-emptive Token Bucket**: Throttled keys are skipped *before* issuing network calls, eliminating retry latency.
59
59
  * **Least-Connections Concurrency Control**: Balances in-flight streams across keys to prevent burst saturation.
60
- * **Autonomous Self-Healing & Cascades**: Proactively tests quarantined keys via canary probes and smoothly escalates to fallback providers if an entire pool is exhausted.
60
+ * **Autonomous Self-Healing & Cascades**: Lifts expired quarantines on idle keys and smoothly escalates to fallback providers if an entire pool is exhausted.
61
61
 
62
62
  ---
63
63
 
@@ -85,8 +85,8 @@ graph LR
85
85
  Failover -.->|All Pool Keys Exhausted| CascadeEngine["Cross-Provider Cascade"]
86
86
 
87
87
  BackoffCalc --> QuotaWindow["Calendar Reset / Midnight Window"]
88
- BackoffCalc --> CanaryProbe["Active Canary Prober (Sandbox Ping)"]
89
- CanaryProbe -->|Verified Healthy| PoolReady["Restored to Ready Pool"]
88
+ BackoffCalc --> SelfHeal["Self-Heal Idle Sweep"]
89
+ SelfHeal -->|Cooldown Expired| PoolReady["Restored to Ready Pool"]
90
90
  end
91
91
 
92
92
  subgraph UpstreamLayer ["Model Provider Endpoints"]
@@ -117,21 +117,18 @@ graph LR
117
117
 
118
118
  ### 🛡️ 3. Autonomous Healing & Cascade Escalation
119
119
  * **Cross-Provider Failover Cascade (`lib/cascade.js`)**: If all keys for a selected provider are in cooldown, requests automatically cascade to an alternative fallback provider pool (e.g., primary provider → fallback proxy / secondary provider).
120
- * **Active Canary Prober (`lib/canary.js`)**: Before releasing a key from quarantine, a lightweight background probe (`/models` probe or single-token check via `SandboxRunner`) validates upstream availability without exposing real user traffic to risk.
120
+ * **Sandbox Key Probes (`lib/sandbox.js`)**: On-demand `/models` probes validate a key before returning it to rotation; idle cooldowns are lifted by the self-heal sweep.
121
121
  * **Calendar & Rolling Quota Reset Windows (`lib/quota-window.js`)**: Supports scheduled quota reset alignments (`midnight_utc`, `midnight_pst`, and `rolling_24h`) so daily free/tier quotas unfreeze exactly when upstream resets them.
122
122
  * **Adaptive Exponential Backoff (`lib/pool.js`)**: Successive failures on a key double its quarantine duration (base → ×2 → ×4 → cap ×8). Successful requests gradually restore healthy status.
123
123
 
124
- ### 🎯 4. Model-Aware & Geolocation Routing
124
+ ### 🎯 4. Model-Aware Routing
125
125
  * **Model Sub-Pools (`lib/pool.js`)**: Configure dedicated key pools for specific model tiers (e.g. reasoning/heavy models vs fast/cheap utility models).
126
126
  * **Tag-Based Routing**: Assign operational tags (`production`, `background`, `eval`) to match key usage with workload priorities.
127
- * **Region Mapping (`lib/region.js`)**: Route queries through geographically optimal credentials and endpoints.
128
127
 
129
128
  ### 📊 5. Observability, Telemetry & Webhooks
130
129
  * **Interactive Multi-Platform Webhooks (`lib/webhook.js`)**: Dispatches rich notifications with HMAC-signed action buttons for **Telegram** (Inline Keyboards), **Discord** (Action Rows), and **Slack** (Block Kit). Administrators can click buttons to reset cooldowns or pause providers directly from their mobile chat.
131
130
  * **Usage & Cost Reporting (`lib/usage-report.js`)**: Per-key daily request counters and estimated cost breakdown with one-click CSV/JSON export (`GET /dsh-key-rotation/usage-report`).
132
131
  * **Latency SLO & Histogram (`lib/histogram.js`)**: Tracks Time-To-First-Token (TTFT) and stream durations with health score degradation scoring (`0..100`).
133
- * **Automated Incident Reporting (`lib/incident.js`)**: Lazily creates structured GitHub Issues on sustained upstream outages.
134
- * **Shadow Traffic Routing (`lib/shadow.js`)**: Fork a configurable percentage of live requests to evaluate secondary providers in shadow mode.
135
132
 
136
133
  ---
137
134
 
@@ -195,7 +192,6 @@ dsh-key-rotation:
195
192
  - UNKNOWN_MODEL
196
193
  - AUTH
197
194
  cooldownMs: 60000
198
- canaryProbing: true
199
195
  concurrencyLimit: 5
200
196
  quotaResetWindow:
201
197
  type: midnight_utc
@@ -224,7 +220,6 @@ dsh-key-rotation:
224
220
  |---|---|---|---|
225
221
  | `switchCodes` | `string[]` | `[QUOTA, RATE_LIMIT, ...]` | List of error codes that immediately trigger failover. |
226
222
  | `cooldownMs` | `number` | `60000` (1 min) | Base penalty duration (in ms) for quarantined keys. |
227
- | `canaryProbing` | `boolean` | `true` | Runs background ping probe before restoring quarantined keys. |
228
223
  | `concurrencyLimit` | `number` | `0` (disabled) | Max concurrent in-flight streams per key (0 = unlimited). |
229
224
  | `quotaResetWindow` | `object` | `null` | Calendar reset alignment (`midnight_utc`, `midnight_pst`, `rolling_24h`). |
230
225
  | `cascade` | `array` | `[]` | Fallback provider chain when primary pool is completely exhausted. |
package/README.ru.md CHANGED
@@ -57,7 +57,7 @@
57
57
  * **Идентичность провайдера остаётся неизменной**: Внутреннее Replay-состояние агента `pi-ai` и контекст инструментов остаются на 100% консистентными.
58
58
  * **Предиктивный Token Bucket**: Перегруженные ключи пропускаются **до** выполнения сетевого запроса, устраняя задержку на сетевой ретрай.
59
59
  * **Балансировка Least-Connections**: Запросы равномерно распределяются по свободным ключам с контролем параллелизма (`maxConcurrency`).
60
- * **Автономное самовосстановление и каскад**: Фоновые canary-зонды проверяют заблокированные ключи, а при полном исчерпании пула запрос бесшовно передаётся запасному провайдеру.
60
+ * **Автономное самовосстановление и каскад**: Просроченные кулдауны простаивающих ключей снимаются автоматически, а при полном исчерпании пула запрос бесшовно передаётся запасному провайдеру.
61
61
 
62
62
  ---
63
63
 
@@ -85,8 +85,8 @@ graph LR
85
85
  Failover -.->|Все ключи в кулдауне| CascadeEngine["Межпровайдерный каскад"]
86
86
 
87
87
  BackoffCalc --> QuotaWindow["Календарный сброс / Полночь UTC/PST"]
88
- BackoffCalc --> CanaryProbe["Active Canary-зонд (Sandbox Ping)"]
89
- CanaryProbe -->|Ключ работоспособен| PoolReady["Возврат в пул готовых ключей"]
88
+ BackoffCalc --> SelfHeal["Self-Heal sweep простаивания"]
89
+ SelfHeal -->|Кулдаун истёк| PoolReady["Возврат в пул готовых ключей"]
90
90
  end
91
91
 
92
92
  subgraph UpstreamLayer ["Эндпоинты провайдеров"]
@@ -117,21 +117,18 @@ graph LR
117
117
 
118
118
  ### 🛡️ 3. Автономное самовосстановление и каскадный Failover
119
119
  * **Межпровайдерный каскад (`lib/cascade.js`)**: При исчерпании всех ключей выбранного провайдера запрос автоматически каскадируется на настроенного резервного провайдера (`cascade: [{ provider, model }]`).
120
- * **Active Canary Prober (`lib/canary.js`)**: Перед выводом ключа из кулдауна плагин выполняет легкий фоновый зонд (`/models` probe через `SandboxRunner`), защищая боевой трафик от повторных сбоев.
120
+ * **Sandbox-пробы ключей (`lib/sandbox.js`)**: По запросу выполняется `/models`-проба ключа перед возвратом в ротацию; простаивающие кулдауны снимаются self-heal sweep.
121
121
  * **Календарный сброс квот (`lib/quota-window.js`)**: Учитывает окна сброса суточных квот провайдеров (`midnight_utc`, `midnight_pst`, `rolling_24h`), снимая карантин ровно в момент обновления лимитов у апстрима.
122
122
  * **Экспоненциальный бэкофф (`lib/pool.js`)**: Повторные сбои на ключе прогрессивно увеличивают время кулдауна (базовое → ×2 → ×4 → максимум ×8).
123
123
 
124
- ### 🎯 4. Маршрутизация по моделям и гео-регионам
124
+ ### 🎯 4. Маршрутизация по моделям
125
125
  * **Модельные подпулы (`lib/pool.js`)**: Назначение выделенных ключей под конкретные модели (например, отдельные ключи для тяжелых reasoning-моделей и дешевые ключи для утилит).
126
126
  * **Тегирование ключей**: Метки приоритета (`production`, `background`, `eval`) для разделения квот между интерактивными и фоновыми задачами.
127
- * **Гео-роутинг (`lib/region.js`)**: Маршрутизация запросов через оптимальные региональные эндпоинты.
128
127
 
129
128
  ### 📊 5. Телеметрия, аналитика и интерактивные вебхуки
130
129
  * **Интерактивные вебхуки (`lib/webhook.js`)**: Отправка форматированных алертов с кнопками действий в **Telegram** (Inline Keyboards), **Discord** (Action Rows) и **Slack** (Block Kit). Администратор может сбросить кулдаун или отключить провайдер прямо из мессенджера.
131
130
  * **Отчеты об использовании и расходах (`lib/usage-report.js`)**: Учет суточного числа запросов и расчетной стоимости по каждому ключу с экспортом в CSV/JSON (`GET /dsh-key-rotation/usage-report`).
132
131
  * **Гистограмма задержек SLO (`lib/histogram.js`)**: Измерение времени до первого токена (TTFT) и расчет индекса здоровья пула (`0..100`).
133
- * **Автоматические инциденты (`lib/incident.js`)**: Создание issue в GitHub при масштабных системных сбоях провайдеров.
134
- * **Shadow-трафик (`lib/shadow.js`)**: Теневое дублирование процента запросов для тестирования альтернативных провайдеров.
135
132
 
136
133
  ---
137
134
 
@@ -195,7 +192,6 @@ dsh-key-rotation:
195
192
  - UNKNOWN_MODEL
196
193
  - AUTH
197
194
  cooldownMs: 60000
198
- canaryProbing: true
199
195
  concurrencyLimit: 5
200
196
  quotaResetWindow:
201
197
  type: midnight_utc
@@ -224,7 +220,6 @@ dsh-key-rotation:
224
220
  |---|---|---|---|
225
221
  | `switchCodes` | `string[]` | `[QUOTA, RATE_LIMIT, ...]` | Список кодов ошибок, инициирующих немедленный переход на следующий ключ. |
226
222
  | `cooldownMs` | `number` | `60000` (1 мин) | Базовая длительность нахождения ключа в карантине (в мс). |
227
- | `canaryProbing` | `boolean` | `true` | Фоновая проверка ключа canary-зондом перед возвратом из карантина. |
228
223
  | `concurrencyLimit` | `number` | `0` (отключено) | Лимит одновременных активных запросов на ключ (0 = без ограничений). |
229
224
  | `quotaResetWindow` | `object` | `null` | Календарное расписание сброса квот (`midnight_utc`, `midnight_pst`, `rolling_24h`). |
230
225
  | `cascade` | `array` | `[]` | Цепочка резервных провайдеров при исчерпании всех ключей основного пула. |
package/README.zh.md CHANGED
@@ -117,7 +117,7 @@ graph LR
117
117
 
118
118
  ### 🛡️ 3. 自动愈合与跨提供商级联
119
119
  * **跨提供商故障转移级联 (`lib/cascade.js`)**:主提供商密钥全部冷却时,自动级联路由到备用提供商池。
120
- * **金丝雀探针探活 (`lib/canary.js`)**:密钥出冷却期前,自动发起轻量探测验证上游可用性,避免影响用户真实请求。
120
+ * **沙箱密钥探测 (`lib/sandbox.js`)**:按需对密钥执行 `/models` 探测后再回到轮换;空闲冷却由 self-heal sweep 解除。
121
121
  * **配额日历重置对齐 (`lib/quota-window.js`)**:支持 `midnight_utc`、`midnight_pst` 与 `rolling_24h` 配额刷新窗口。
122
122
  * **自适应指数退避 (`lib/pool.js`)**:连续失败使冷却时间呈指数递增(基准 → ×2 → ×4 → 上限 ×8)。
123
123
 
@@ -125,7 +125,6 @@ graph LR
125
125
  * **交互式 Webhook (`lib/webhook.js`)**:向 **Telegram**、**Discord**、**Slack** 推送带交互按钮的富文本警报,可在移动聊天中一键重置冷却或暂停提供商。
126
126
  * **使用量与成本报表 (`lib/usage-report.js`)**:按日统计各密钥请求数与预估成本,支持一键导出 CSV/JSON (`GET /dsh-key-rotation/usage-report`)。
127
127
  * **延迟 SLO 监控 (`lib/histogram.js`)**:记录首字延迟(TTFT)与健康度评分 (`0..100`)。
128
- * **影子流量测试 (`lib/shadow.js`)**:支持配置百分比的流量镜像复制以评估次要提供商。
129
128
 
130
129
  ---
131
130
 
@@ -185,7 +184,6 @@ dsh-key-rotation:
185
184
  - UNKNOWN_MODEL
186
185
  - AUTH
187
186
  cooldownMs: 60000
188
- canaryProbing: true
189
187
  concurrencyLimit: 5
190
188
  quotaResetWindow:
191
189
  type: midnight_utc
@@ -0,0 +1,169 @@
1
+ // lib/http-bridge.js — shared HTTP helpers for dsh-key-rotation routes.
2
+ import { findSecrets } from './keycheck.js';
3
+ import { isTrustedBridgeRequest } from './pool.js';
4
+
5
+ export const NS = 'dsh-key-rotation';
6
+
7
+ export function json(res, status, obj) {
8
+ res.writeHead(status, { 'content-type': 'application/json' });
9
+ res.end(JSON.stringify(obj));
10
+ }
11
+
12
+ export function readJson(request) {
13
+ return new Promise((resolve, reject) => {
14
+ let raw = '';
15
+ request.on('data', (c) => { raw += c; });
16
+ request.on('end', () => {
17
+ try {
18
+ resolve(JSON.parse(raw || '{}'));
19
+ } catch (e) {
20
+ reject(e);
21
+ }
22
+ });
23
+ request.on('error', reject);
24
+ });
25
+ }
26
+
27
+ export function descriptorOf(ctx, ns) {
28
+ const settings = ctx.get('settings');
29
+ if (settings === void 0) return void 0;
30
+ return settings.describe({ redactSecrets: true }).find((candidate) => candidate.ns === ns);
31
+ }
32
+
33
+ export function viewOf(descriptor, settings) {
34
+ return {
35
+ available: true,
36
+ writable: settings.writable,
37
+ hasDocument: settings.hasDocument,
38
+ value: descriptor.value,
39
+ ...descriptor.base === void 0 ? {} : { base: descriptor.base },
40
+ ...descriptor.user === void 0 || Object.keys(descriptor.user).length === 0 ? {} : { user: descriptor.user },
41
+ revision: descriptor.revision,
42
+ };
43
+ }
44
+
45
+ export async function writeSection(ctx, ns, section, expectedRevision, res) {
46
+ const settings = ctx.get('settings');
47
+ if (settings === void 0) {
48
+ json(res, 503, { error: { code: 'settings-rejected', message: 'dsh-key-rotation: no settings provider is mounted' } });
49
+ return;
50
+ }
51
+ try {
52
+ await settings.replace(ns, section, expectedRevision);
53
+ } catch (error) {
54
+ if (error?.code === 'SETTINGS_CONFLICT') {
55
+ json(res, 409, { error: { code: 'settings-conflict', message: `dsh-key-rotation: changed elsewhere (expected revision ${String(error.expected)}, current ${String(error.actual)}); reload and retry` } });
56
+ return;
57
+ }
58
+ json(res, 400, { error: { code: 'settings-rejected', message: error instanceof Error ? error.message : String(error) } });
59
+ return;
60
+ }
61
+ const descriptor = descriptorOf(ctx, ns);
62
+ if (descriptor === void 0) {
63
+ json(res, 500, { error: { code: 'settings-rejected', message: 'dsh-key-rotation: namespace vanished after write' } });
64
+ return;
65
+ }
66
+ json(res, 200, viewOf(descriptor, { writable: settings.writable, hasDocument: settings.documentPath !== void 0 }));
67
+ }
68
+
69
+ export function providerCatalog(ctx, cloneIds) {
70
+ const seen = new Set();
71
+ const out = [];
72
+ for (const info of ctx.llm.listProviders()) {
73
+ if (seen.has(info.id) || cloneIds.has(info.id)) continue;
74
+ seen.add(info.id);
75
+ out.push({ id: info.id, name: info.name ?? info.id });
76
+ }
77
+ return out;
78
+ }
79
+
80
+ export function guardLocal(req, res, label) {
81
+ if (!isTrustedBridgeRequest(req)) {
82
+ json(res, 403, { error: { code: 'forbidden', message: `dsh-key-rotation: ${label} is local-only` } });
83
+ return false;
84
+ }
85
+ return true;
86
+ }
87
+
88
+ export function scanForLiveSecrets(section) {
89
+ const masked = structuredClone(section);
90
+ if (masked.webhookActionToken) masked.webhookActionToken = '***';
91
+ if (masked.notifyWebhook) masked.notifyWebhook = '***';
92
+ return findSecrets(JSON.stringify(masked));
93
+ }
94
+
95
+ /** Config bridge GET/PUT/DELETE — extracted from lib/index.js for size (#253). */
96
+ export async function handleConfigBridge(ctx, request, res, getCloneIds) {
97
+ if (!isTrustedBridgeRequest(request)) {
98
+ res.writeHead(403);
99
+ res.end();
100
+ return;
101
+ }
102
+ const method = request.method ?? 'GET';
103
+ if (method === 'GET') {
104
+ const settings = ctx.get('settings');
105
+ const descriptor = descriptorOf(ctx, NS);
106
+ const body = {
107
+ providers: providerCatalog(ctx, getCloneIds()),
108
+ };
109
+ if (descriptor === void 0) {
110
+ json(res, 200, {
111
+ ...body,
112
+ available: false,
113
+ writable: settings?.writable ?? false,
114
+ hasDocument: settings?.documentPath !== void 0,
115
+ value: void 0,
116
+ revision: 0,
117
+ });
118
+ return;
119
+ }
120
+ json(res, 200, {
121
+ ...body,
122
+ ...viewOf(descriptor, {
123
+ writable: settings?.writable ?? false,
124
+ hasDocument: settings?.documentPath !== void 0,
125
+ }),
126
+ });
127
+ return;
128
+ }
129
+ if (method === 'PUT' || method === 'DELETE') {
130
+ let section;
131
+ let expectedRevision;
132
+ if (method === 'PUT') {
133
+ let body;
134
+ try {
135
+ body = await readJson(request);
136
+ } catch (error) {
137
+ json(res, 400, { error: { code: 'settings-rejected', message: `dsh-key-rotation: invalid request body: ${error instanceof Error ? error.message : String(error)}` } });
138
+ return;
139
+ }
140
+ if (typeof body !== 'object' || body === null || typeof body.section !== 'object' || body.section === null || Array.isArray(body.section)) {
141
+ json(res, 400, { error: { code: 'settings-rejected', message: 'dsh-key-rotation: PUT requires {"section": {...}}' } });
142
+ return;
143
+ }
144
+ section = body.section;
145
+ expectedRevision = typeof body.expectedRevision === 'number' ? body.expectedRevision : void 0;
146
+ try {
147
+ const findings = scanForLiveSecrets(section);
148
+ if (findings.length > 0) {
149
+ json(res, 400, {
150
+ error: {
151
+ code: 'secret-in-config',
152
+ message: `dsh-key-rotation: value looks like a live credential (${findings[0].type}); store key values via the key field, not the config section`,
153
+ findings,
154
+ },
155
+ });
156
+ return;
157
+ }
158
+ } catch {
159
+ /* scanning must never block a valid save */
160
+ }
161
+ } else {
162
+ section = {};
163
+ expectedRevision = void 0;
164
+ }
165
+ await writeSection(ctx, NS, section, expectedRevision, res);
166
+ return;
167
+ }
168
+ json(res, 405, { error: { code: 'method', message: 'GET, PUT, or DELETE only' } });
169
+ }