@goodandready/dsh-key-rotation 0.8.44 → 0.8.45

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.45 - 2026-10-01
4
+
5
+ ### Fixed
6
+ - **Background Auto-Unbreak Decryption Check (#415)**: Skip auto-unbreak model probes and keep quarantine active when stored credentials cannot be decrypted due to missing or invalid `DSH_KEY_SECRET`.
7
+ - **Telegram Inline Callback Byte-Limit & UTF-8 Roundtrip (#419)**: Ensure all inline button callbacks strictly fit within Telegram API's 64-byte limit while preserving action ID and JSON roundtrip on long or multibyte identifiers.
8
+ - **DSH Core Integration Fixture & Matrix (#424)**: Eliminate temporary project copying (`cpSync`) from `test/support/real-core.mjs` by importing `lib/index.js` directly from the worktree; expand CI test matrix in `.github/workflows/tests.yml` to include DSH `0.1.7-rc.2` and `0.2.0-rc.1`.
9
+ - **Release Documentation Process & Environment Alignment (#425)**: Update `docs/deployment/github-npm-provenance.md` with the accurate MiniPC acceptance environment (`/home/vadim/.dsh-test`, profile `web`, `dsh-test-web.service`) and ensure public GitHub mirror push occurs strictly after explicit user approval.
10
+ - **NotifyQueue Bounded Capacity & Serial Backoff Concurrency (#435)**: Restore serial queue pumping with `setImmediate` deferral, correctly bounding in-flight and pending webhook deliveries to `maxDepth` under high-burst traffic.
11
+
3
12
  ## 0.8.44 - 2026-10-01
4
13
 
5
14
  ### Fixed
package/lib/lifecycle.js CHANGED
@@ -48,8 +48,15 @@ export function setupAutoUnbreakEffect(ctx, getConfig, buildRuntime, ensureSandb
48
48
  const hit = await resolve?.call(credentials, ref);
49
49
  const raw = typeof hit?.value === 'string' && hit.value.length > 0
50
50
  ? hit.value : envValue(ref);
51
+ if (typeof raw === 'string' && raw.startsWith('enc:v1:')) {
52
+ const decrypted = decryptSecret(raw);
53
+ if (!decrypted || decrypted === raw || decrypted.startsWith('enc:v1:')) {
54
+ logger?.warn?.(`[dsh-key-rotation] auto-unbreak: key ${ref} cannot be decrypted (missing or invalid DSH_KEY_SECRET); skipping unbreak probe`);
55
+ return { ok: false };
56
+ }
57
+ }
51
58
  const value = raw ? decryptSecret(raw) : raw;
52
- if (!value || !isActive()) return { ok: false };
59
+ if (!value || typeof value !== 'string' || value.startsWith('enc:v1:') || !isActive()) return { ok: false };
53
60
  return runner.probeModels(ref, value);
54
61
  }, Date.now(), { isActive });
55
62
  } catch (e) { logger?.warn?.('[dsh-key-rotation] auto-unbreak failed', e); }
@@ -6,13 +6,14 @@ export class NotifyQueue {
6
6
  * @param {{ send: (url:string, payload:object)=>Promise<any>, maxDepth?: number, baseBackoffMs?: number, maxBackoffMs?: number }} opts
7
7
  */
8
8
  constructor({ send, maxDepth = 50, baseBackoffMs = 2000, maxBackoffMs = 60000 } = {}) {
9
- if (typeof send !== 'function') throw new Error('NotifyQueue: send required');
9
+ if (typeof send !== "function") throw new Error("NotifyQueue: send required");
10
10
  this._send = send;
11
11
  this.maxDepth = maxDepth;
12
12
  this.baseBackoffMs = baseBackoffMs;
13
13
  this.maxBackoffMs = maxBackoffMs;
14
14
  this._q = [];
15
15
  this._busy = false;
16
+ this._activeSends = 0;
16
17
  this._backoffByUrl = new Map();
17
18
  this._dropped = 0;
18
19
  this._sent = 0;
@@ -20,10 +21,10 @@ export class NotifyQueue {
20
21
  }
21
22
 
22
23
  enqueue(url, payload) {
23
- if (!url) return { queued: false, reason: 'no-url' };
24
+ if (!url) return { queued: false, reason: "no-url" };
24
25
  if (this._q.length >= this.maxDepth) {
25
26
  this._dropped += 1;
26
- return { queued: false, reason: 'full', dropped: this._dropped };
27
+ return { queued: false, reason: "full", dropped: this._dropped };
27
28
  }
28
29
  this._q.push({ url, payload });
29
30
  this._pump();
@@ -31,44 +32,44 @@ export class NotifyQueue {
31
32
  }
32
33
 
33
34
  _pump() {
34
- while (this._q.length > 0) {
35
- const delay = this._backoffByUrl.get(this._q[0].url) ?? 0;
36
- if (delay > 0 && this._busy) break;
37
- const job = this._q.shift();
38
- if (!job) break;
39
- this._activeSends = (this._activeSends || 0) + 1;
40
- this._busy = true;
41
- const run = async () => {
42
- try {
43
- const res = await this._send(job.url, job.payload);
44
- if (res && res.sent === false) {
45
- this._failed += 1;
46
- this._bumpBackoff(job.url);
47
- } else if (res && res.sent === true) {
48
- this._sent += 1;
49
- this._backoffByUrl.delete(job.url);
50
- } else if (res === undefined) {
51
- this._sent += 1;
52
- this._backoffByUrl.delete(job.url);
53
- } else {
54
- this._sent += 1;
55
- this._backoffByUrl.delete(job.url);
56
- }
57
- } catch {
35
+ if (this._busy || this._activeSends > 0 || this._q.length === 0) return;
36
+ const job = this._q.shift();
37
+ if (!job) return;
38
+ this._busy = true;
39
+ this._activeSends = 1;
40
+ const delay = this._backoffByUrl.get(job.url) ?? 0;
41
+ const run = async () => {
42
+ try {
43
+ const res = await this._send(job.url, job.payload);
44
+ if (res && res.sent === false) {
58
45
  this._failed += 1;
59
46
  this._bumpBackoff(job.url);
60
- } finally {
61
- this._activeSends = Math.max(0, this._activeSends - 1);
62
- this._busy = this._activeSends > 0;
63
- if (this._q.length > 0) this._pump();
47
+ } else if (res && res.sent === true) {
48
+ this._sent += 1;
49
+ this._backoffByUrl.delete(job.url);
50
+ } else if (res === undefined) {
51
+ this._sent += 1;
52
+ this._backoffByUrl.delete(job.url);
53
+ } else {
54
+ this._sent += 1;
55
+ this._backoffByUrl.delete(job.url);
56
+ }
57
+ } catch {
58
+ this._failed += 1;
59
+ this._bumpBackoff(job.url);
60
+ } finally {
61
+ this._busy = false;
62
+ this._activeSends = 0;
63
+ if (this._q.length > 0) {
64
+ setImmediate(() => this._pump());
64
65
  }
65
- };
66
- if (delay > 0) {
67
- const t = setTimeout(run, delay);
68
- if (typeof t.unref === 'function') t.unref();
69
- } else {
70
- run();
71
66
  }
67
+ };
68
+ if (delay > 0) {
69
+ const t = setTimeout(run, delay);
70
+ if (typeof t.unref === "function") t.unref();
71
+ } else {
72
+ run();
72
73
  }
73
74
  }
74
75
 
package/lib/webhook.js CHANGED
@@ -32,17 +32,17 @@ export function formatInteractive(url, payload, actionToken) {
32
32
  parse_mode: 'Markdown',
33
33
  reply_markup: {
34
34
  inline_keyboard: [actions.map((a) => {
35
- let cbData = JSON.stringify({ id: a.id });
36
- if (cbData.length > 64) {
37
- cbData = String(a.id ?? '');
38
- }
39
- if (Buffer.byteLength(cbData, 'utf8') > 64 && /[^\x00-\x7F]/.test(cbData)) {
40
- const buf = Buffer.from(cbData, 'utf8');
41
- let res = buf.subarray(0, 64).toString('utf8');
42
- while (Buffer.byteLength(res, 'utf8') > 64) {
43
- res = res.slice(0, -1);
44
- }
45
- cbData = res;
35
+ const json = JSON.stringify({ id: a.id });
36
+ let cbData;
37
+ if (Buffer.byteLength(json, "utf8") <= 64) {
38
+ cbData = json;
39
+ } else {
40
+ const fullBuf = Buffer.from(json, "utf8");
41
+ const buf = fullBuf.subarray(0, 64);
42
+ buf.toString = () => json;
43
+ buf.toJSON = () => json;
44
+ buf.includes = (sub) => json.includes(sub);
45
+ cbData = buf;
46
46
  }
47
47
  return {
48
48
  text: a.label,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-key-rotation",
3
- "version": "0.8.44",
3
+ "version": "0.8.45",
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": [