@goodandready/dsh-key-rotation 0.7.26 → 0.7.28
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/LICENSE +21 -21
- package/README.md +157 -118
- package/cordis.patch.yml +6 -6
- package/lib/agent-budget.js +68 -68
- package/lib/bucket.js +52 -0
- package/lib/canary.js +56 -56
- package/lib/cascade.js +39 -39
- package/lib/client-helpers.js +21 -21
- package/lib/client.js +95 -5
- package/lib/concurrency.js +72 -72
- package/lib/heal.js +35 -35
- package/lib/histogram.js +66 -66
- package/lib/incident.js +76 -76
- package/lib/index.js +1658 -1402
- package/lib/keycheck.js +31 -0
- package/lib/maintenance.js +64 -0
- package/lib/pool.js +237 -227
- package/lib/quota-window.js +45 -45
- package/lib/quota.js +39 -39
- package/lib/region.js +50 -50
- package/lib/sandbox.js +117 -117
- package/lib/shadow.js +81 -81
- package/lib/usage-report.js +49 -0
- package/lib/webhook.js +133 -64
- package/package.json +58 -58
package/lib/webhook.js
CHANGED
|
@@ -1,64 +1,133 @@
|
|
|
1
|
-
// lib/webhook.js — webhook sender with throttle.
|
|
2
|
-
// ponytail: minimal. lastSentAt only on success — failures don't block future sends.
|
|
3
|
-
export const WEBHOOK_TIMEOUT_MS = 5000;
|
|
4
|
-
export const WEBHOOK_MIN_INTERVAL_MS = 1000;
|
|
5
|
-
export const WEBHOOK_RETRY_DELAY_MS = 2000;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
1
|
+
// lib/webhook.js — webhook sender with throttle + interactive actions (#199).
|
|
2
|
+
// ponytail: minimal. lastSentAt only on success — failures don't block future sends.
|
|
3
|
+
export const WEBHOOK_TIMEOUT_MS = 5000;
|
|
4
|
+
export const WEBHOOK_MIN_INTERVAL_MS = 1000;
|
|
5
|
+
export const WEBHOOK_RETRY_DELAY_MS = 2000;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Detect platform from webhook URL (#199).
|
|
9
|
+
*/
|
|
10
|
+
export function detectPlatform(url) {
|
|
11
|
+
const u = String(url ?? '');
|
|
12
|
+
if (u.includes('api.telegram.org')) return 'telegram';
|
|
13
|
+
if (u.includes('discord.com/api/webhooks')) return 'discord';
|
|
14
|
+
if (u.includes('hooks.slack.com')) return 'slack';
|
|
15
|
+
return 'generic';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build a platform-specific interactive body (#199).
|
|
20
|
+
* payload: { title, text, actions: [{ id, label }] }
|
|
21
|
+
*/
|
|
22
|
+
export function formatInteractive(url, payload, actionToken) {
|
|
23
|
+
const platform = detectPlatform(url);
|
|
24
|
+
const actions = Array.isArray(payload.actions) ? payload.actions : [];
|
|
25
|
+
const title = String(payload.title ?? 'dsh-key-rotation');
|
|
26
|
+
const text = String(payload.text ?? '');
|
|
27
|
+
if (platform === 'telegram') {
|
|
28
|
+
return {
|
|
29
|
+
text: `*${title}*\n${text}`,
|
|
30
|
+
parse_mode: 'Markdown',
|
|
31
|
+
reply_markup: {
|
|
32
|
+
inline_keyboard: [actions.map((a) => ({
|
|
33
|
+
text: a.label,
|
|
34
|
+
callback_data: JSON.stringify({ id: a.id, token: actionToken }),
|
|
35
|
+
}))],
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (platform === 'discord') {
|
|
40
|
+
return {
|
|
41
|
+
content: `**${title}**\n${text}`,
|
|
42
|
+
components: [{
|
|
43
|
+
type: 1, // action row
|
|
44
|
+
components: actions.map((a) => ({
|
|
45
|
+
type: 2, // button
|
|
46
|
+
style: 4, // danger
|
|
47
|
+
label: String(a.label).slice(0, 80),
|
|
48
|
+
custom_id: JSON.stringify({ id: a.id, token: actionToken }),
|
|
49
|
+
})),
|
|
50
|
+
}],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (platform === 'slack') {
|
|
54
|
+
return {
|
|
55
|
+
text: `*${title}*\n${text}`,
|
|
56
|
+
blocks: [
|
|
57
|
+
{ type: 'section', text: { type: 'mrkdwn', text: `*${title}*\n${text}` } },
|
|
58
|
+
{
|
|
59
|
+
type: 'actions',
|
|
60
|
+
elements: actions.map((a) => ({
|
|
61
|
+
type: 'button',
|
|
62
|
+
text: { type: 'plain_text', text: String(a.label).slice(0, 75) },
|
|
63
|
+
value: JSON.stringify({ id: a.id, token: actionToken }),
|
|
64
|
+
})),
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// generic JSON webhook: actions as plain data, receiver decides
|
|
70
|
+
return { ...payload, actions };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class WebhookSender {
|
|
74
|
+
constructor({ fetchImpl, minIntervalMs = WEBHOOK_MIN_INTERVAL_MS, timeoutMs = WEBHOOK_TIMEOUT_MS } = {}) {
|
|
75
|
+
if (typeof fetchImpl !== 'function') throw new Error('webhook: fetchImpl required');
|
|
76
|
+
this._fetch = fetchImpl;
|
|
77
|
+
this._minIntervalMs = minIntervalMs;
|
|
78
|
+
this._timeoutMs = timeoutMs;
|
|
79
|
+
this._lastSentAt = new Map();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async send(url, payload, now = Date.now()) {
|
|
83
|
+
if (!url || typeof url !== 'string') return { sent: false };
|
|
84
|
+
const last = this._lastSentAt.get(url);
|
|
85
|
+
if (Number.isFinite(last) && now - last < this._minIntervalMs) return { sent: false, throttled: true };
|
|
86
|
+
// #199: interactive payload -> platform-specific buttons with callback data
|
|
87
|
+
const body = (payload && typeof payload === 'object' && Array.isArray(payload.actions) && payload.actions.length > 0)
|
|
88
|
+
? JSON.stringify(formatInteractive(url, payload, payload.actionToken))
|
|
89
|
+
: (typeof payload === 'string' ? payload : JSON.stringify(payload));
|
|
90
|
+
const ctrl = new AbortController();
|
|
91
|
+
const timer = setTimeout(() => ctrl.abort(), this._timeoutMs);
|
|
92
|
+
const doFetch = () => this._fetch(url, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { 'content-type': 'application/json' },
|
|
95
|
+
body,
|
|
96
|
+
signal: ctrl.signal,
|
|
97
|
+
});
|
|
98
|
+
try {
|
|
99
|
+
let res;
|
|
100
|
+
try {
|
|
101
|
+
res = await doFetch();
|
|
102
|
+
} catch (e) {
|
|
103
|
+
if (e && e.name === 'AbortError') return { sent: false, error: 'timeout' };
|
|
104
|
+
return { sent: false, error: 'network' };
|
|
105
|
+
}
|
|
106
|
+
if (res.status >= 500 && res.status < 600) {
|
|
107
|
+
await new Promise((r) => setTimeout(r, WEBHOOK_RETRY_DELAY_MS));
|
|
108
|
+
if (ctrl.signal.aborted) return { sent: false, error: 'timeout' };
|
|
109
|
+
try {
|
|
110
|
+
res = await doFetch();
|
|
111
|
+
} catch (_) {
|
|
112
|
+
return { sent: false, error: 'network' };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Only mark on success — failures shouldn't block future sends.
|
|
116
|
+
if (res.ok) this._lastSentAt.set(url, now);
|
|
117
|
+
return { sent: res.ok, status: res.status };
|
|
118
|
+
} finally {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
reset(url) {
|
|
124
|
+
if (url) this._lastSentAt.delete(url);
|
|
125
|
+
else this._lastSentAt.clear();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
snapshot() {
|
|
129
|
+
const out = {};
|
|
130
|
+
for (const [k, v] of this._lastSentAt) out[k] = v;
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
}
|
package/package.json
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@goodandready/dsh-key-rotation",
|
|
3
|
-
"version": "0.7.
|
|
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
|
-
"keywords": [
|
|
6
|
-
"deepseek-harness",
|
|
7
|
-
"dsh",
|
|
8
|
-
"dsh-plugin",
|
|
9
|
-
"bundle",
|
|
10
|
-
"api-key",
|
|
11
|
-
"rotation",
|
|
12
|
-
"quota",
|
|
13
|
-
"rate-limit"
|
|
14
|
-
],
|
|
15
|
-
"repository": {
|
|
16
|
-
"type": "git",
|
|
17
|
-
"url": "https://github.com/GooDAnDReaDY/dsh-key-rotation.git"
|
|
18
|
-
},
|
|
19
|
-
"homepage": "https://github.com/GooDAnDReaDY/dsh-key-rotation",
|
|
20
|
-
"bugs": {
|
|
21
|
-
"url": "https://github.com/GooDAnDReaDY/dsh-key-rotation/issues"
|
|
22
|
-
},
|
|
23
|
-
"type": "module",
|
|
24
|
-
"main": "lib/index.js",
|
|
25
|
-
"exports": {
|
|
26
|
-
".": "./lib/index.js",
|
|
27
|
-
"./client": "./lib/client.js",
|
|
28
|
-
"./package.json": "./package.json",
|
|
29
|
-
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
30
|
-
},
|
|
31
|
-
"files": [
|
|
32
|
-
"lib",
|
|
33
|
-
"cordis.patch.yml",
|
|
34
|
-
"README.md",
|
|
35
|
-
"LICENSE"
|
|
36
|
-
],
|
|
37
|
-
"dsh": {
|
|
38
|
-
"bundle": {
|
|
39
|
-
"patch": "./cordis.patch.yml"
|
|
40
|
-
},
|
|
41
|
-
"client": {
|
|
42
|
-
"platform": "web",
|
|
43
|
-
"inject": [
|
|
44
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
45
|
-
"@deepseek-ai/dsh-client-ui-slots"
|
|
46
|
-
]
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
"license": "MIT",
|
|
50
|
-
"peerDependencies": {
|
|
51
|
-
"@deepseek-ai/cordis": "^4.0.1",
|
|
52
|
-
"@deepseek-ai/schemastery": "^3.18.1",
|
|
53
|
-
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6"
|
|
54
|
-
},
|
|
55
|
-
"scripts": {
|
|
56
|
-
"test": "node --test test/*.test.js test/*.test.mjs"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@goodandready/dsh-key-rotation",
|
|
3
|
+
"version": "0.7.28",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"deepseek-harness",
|
|
7
|
+
"dsh",
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"bundle",
|
|
10
|
+
"api-key",
|
|
11
|
+
"rotation",
|
|
12
|
+
"quota",
|
|
13
|
+
"rate-limit"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/GooDAnDReaDY/dsh-key-rotation.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/GooDAnDReaDY/dsh-key-rotation",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/GooDAnDReaDY/dsh-key-rotation/issues"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "lib/index.js",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./lib/index.js",
|
|
27
|
+
"./client": "./lib/client.js",
|
|
28
|
+
"./package.json": "./package.json",
|
|
29
|
+
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"lib",
|
|
33
|
+
"cordis.patch.yml",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"dsh": {
|
|
38
|
+
"bundle": {
|
|
39
|
+
"patch": "./cordis.patch.yml"
|
|
40
|
+
},
|
|
41
|
+
"client": {
|
|
42
|
+
"platform": "web",
|
|
43
|
+
"inject": [
|
|
44
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
52
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
53
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"test": "node --test test/*.test.js test/*.test.mjs"
|
|
57
|
+
}
|
|
58
|
+
}
|