@goodandready/dsh-key-rotation 0.8.12 → 0.8.14

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 ADDED
@@ -0,0 +1,216 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@goodandready/dsh-key-rotation` are documented here.
4
+ User-facing feature notes also appear in README (en is source of truth).
5
+
6
+ ## 0.8.14 - 2026-09-19
7
+
8
+ ### Fixed
9
+ - **Critical fix: switchable error rotation crash (#324, GitHub #3 / PR #4)**: `isSwitchableError()` in `lib/pool.js` now normalizes `switchCodes` to a `Set` when passed as an array. Previously, runtime schema arrays (`cfg.switchCodes`, `DEFAULT_SWITCH_CODES`) caused `TypeError: switchCodes.has is not a function`, crashing turns on rate limits, quota exhaustion, and 5xx errors instead of rotating keys.
10
+ - **Critical fix: settings UI render crash (#324, GitHub #2 / PR #5)**: declared `UpdaterSection` at factory scope in `lib/client.js` before `KeyRotationSection`, removing the `apply(ctx)`-local declaration that caused `ReferenceError: UpdaterSection is not defined`.
11
+ - **Security hardening: packageSpec validation (#324, GitHub #6)**: added strict package spec regex validation in `lib/plugin-updater.js` `installExact()` before child process invocation, preventing command-option injection.
12
+ - **Settings reachable again**: the card registered into `settings.plugin.item`, a
13
+ slot the current DSH core (0.1.6-alpha.2) no longer renders, so the plugin's
14
+ settings were unreachable. The surface now registers into the Plugins page row
15
+ seat `plugins.row.config` first, keyed
16
+ `@goodandready/dsh-key-rotation#dsh-key-rotation` (`rowConfigKey(package, rowId)`):
17
+ the plugin's row gains a configure control whose page is the settings form
18
+ (`view: 'page'`, open and without our card chrome — the host page draws the title,
19
+ icon, crumb and padding) plus a one-line state for `view: 'summary'`. The legacy
20
+ seat stays registered as a fallback for older cores.
21
+
22
+ Version bump and public release are intentionally held back for now: the feature
23
+ work on `feat/issue-303-v089-evolution` is still in flight, so this change lands in
24
+ `main` without a release.
25
+
26
+ ## 0.8.13 - 2026-09-17
27
+
28
+ ### Changed
29
+ - **Server Decomposition (#312)**: `lib/index.js` decomposed from 924 to ~562 lines to meet the 600-line plugin authoring threshold. Extracted modular services into `lib/logger.js`, `lib/sandbox-service.js`, `lib/budget-monitor.js`, `lib/pool-builder.js`, and `lib/lifecycle.js`. Public exports and runtime behavior are 100% preserved.
30
+ - **Logging Alignment**: replaced all runtime `console.warn` calls across server modules (`lib/index.js`, `lib/rotate.js`, `lib/ops-status.js`, `lib/ops-webhook.js`) with Cordis scoped logger (`ctx.logger('key-rotation')` / `getLogger(ctx)`).
31
+
32
+ ### Fixed
33
+ - **Preflight & Theme Conformance (#312)**:
34
+ - Client UI: eliminated 11 hardcoded fallback hex color literals from `lib/client.js`, strictly adopting `--dsw-alias-state-*` tokens.
35
+ - Client UI: requested `IconChevronDownOutline14` in module header to satisfy preflight icon contract.
36
+ - HTTP Bridge: added strict HTTP method guard (`GET`, `PUT`, `DELETE`, `OPTIONS`) on `/api/dsh-key-rotation/config` write-capable routes.
37
+ - Preflight score improved to `FAIL: 0, WARN: 1` (only `lib/client.js` exceeding 600 lines as documented single-file browser bundle).
38
+
39
+ ### Verification
40
+ - unit: 375 pass / 0 fail / 1 skipped
41
+ - `bash preflight.sh`: FAIL: 0, WARN: 1
42
+
43
+ ## 0.8.12 - 2026-09-16
44
+
45
+ ### Changed
46
+ - **Internal decompose (#312)**: operational HTTP routes split from a single `lib/routes-ops.js` facade into focused modules (`lib/ops-status.js`, `ops-telemetry.js`, `ops-keys.js`, `ops-test.js`, `ops-webhook.js`, shared `lib/ops-paths.js`). Public entry `registerOpsRoutes` is unchanged.
47
+ - Notify helpers (`notifySwitch`, `pushEvent`, `notifyExhaustion`) extracted to `lib/notify-events.js`; `lib/index.js` re-exports the public pair.
48
+ - `lib/client.js` intentionally remains a single ModuleLoader factory (no bundler contract); pure helpers stay in `lib/client-helpers.js`.
49
+
50
+ ### Fixed
51
+ - `stability-hardening` inject mock now provides `sctx.get` so the settings registration path runs when the schemastery peer is present.
52
+
53
+ ### Notes
54
+ - No user-facing behavior change: same HTTP paths, same rotation/notify semantics, same settings card.
55
+ - `lib/index.js` `apply()` remains a single cordis wiring unit by design.
56
+
57
+ ### Packaging
58
+ - New runtime modules under `lib/` are included via existing `files: ["lib", ...]`.
59
+
60
+ ### Verification
61
+ - unit: 377 pass / 0 fail
62
+ - PR #319 merged to main (`4d78853`)
63
+
64
+ ## 0.8.11 - 2026-09-16
65
+
66
+ ### Added
67
+ - **One-click plugin updater** (#307): host endpoint `GET/POST /api/dsh-key-rotation/update` and settings UI section to check the latest npm version and install it via the standard `dsh plugin add` path. POST is loopback + same-origin gated; GET exposes version metadata only.
68
+ - `lib/best-effort.js` helper for intentional non-critical side effects (sync + async, debug log, never throws) (#315).
69
+ - `dsh.client.inject` declares `@deepseek-ai/dsh-client-locale` and `@deepseek-ai/dsh-client-ui-settings` (#313).
70
+ - Unit tests: `test/plugin-updater-307.test.mjs`, `test/best-effort.test.mjs`.
71
+
72
+ ### Changed
73
+ - Client styles use theme tokens via `color-mix` / `--krot_chart-*` instead of hard-coded rgba/hex (#311).
74
+ - Cyrillic comments in `lib/client.js` translated to English; locale note documents en+zh (#309).
75
+ - DESIGN.md locks: ModuleLoader single-file client constraint; short cordis `export const name` vs scoped npm identity; bestEffort policy (#312, #315).
76
+
77
+ ### Fixed
78
+ - Empty `catch` blocks that swallowed persistence, circuit-reset, webhook-callback and UI errors now go through `bestEffort` with debug logging (#315).
79
+ - Production-path tests retargeted after removal of dead exports (`isSoftFailure`, `TokenBucketAccumulator`, etc.) and `test/bucket-o1.test.mjs` (#314).
80
+
81
+ ### Removed
82
+ - Internal agent files (`AGENTS.md`, `index.md`, `docs/plans/*`) from the git publication set and npm pack (#308).
83
+ - Stale release `.tgz` artifacts from the DEV tree (#310).
84
+
85
+ ### Packaging
86
+ - `npm pack`: 34 files, max file ~105KB (`lib/client.js`), no AGENTS/index/docs/plans/openwiki.
87
+
88
+ ### Verification
89
+ - unit: 248 pass / 0 fail / 1 skip
90
+ - preflight: empty-catch FAIL cleared; remaining name FAIL is accepted short cordis id (image-gen convention)
91
+ - MiniPC test contour: status 200, updater GET 200, POST foreign/missing origin 403, cleanup OK
92
+ - Production candidate (temporary tgz): status 200, providers=2 keys=7 present=7, updater GET 200, POST 403 on non-local origin
93
+
94
+ ## 0.8.8 - 2026-09-12
95
+
96
+ - feat(ui): add dedicated `.krot-header` with live status badges (pools count, keys configured, circuit state, health score) and subtitle matching `dsh-clinebot` styling (#301, #302)
97
+ - fix(ops): resolve `ReferenceError: quotaStore is not defined` in `GET /dsh-key-rotation/health` by passing dependency from `index.js` and adding defensive invocation (#301, #302)
98
+ - fix(ops): import `isLoopbackAddress` in `routes-ops.js` for local loopback verification without Origin header (#301, #302)
99
+ - fix(ops): guard `lastTestCache.snapshot` in `GET /dsh-key-rotation/sandbox-cache` against Map and mock instances (#301, #302)
100
+ - fix(ops): synchronize provider reset in `POST /dsh-key-rotation/webhook-action` with `RESET_PATH` to clear `authFailCounts`, reset `switches`, and reset `circuitBreaker` (#301, #302)
101
+ - fix(ops): single-key reset in `POST /dsh-key-rotation/reset` now checks and clears `brokenUntil` and `authFailCounts` even when cooldown has elapsed (#301, #302)
102
+ - fix(ops): clean up orphan entries from `poolState` and `lastTestCache` upon key deletion via `DELETE /dsh-key-rotation/key` (#301, #302)
103
+ - test: add comprehensive test suites `test/routes-ops-comprehensive.test.mjs` (12 tests) and `test/http-bridge-config.test.mjs` (5 tests), increasing overall test coverage to 89.40% (359 total unit tests) (#301, #302)
104
+
105
+ ## 0.8.7 - 2026-09-12
106
+
107
+ - fix(client): resolve `t is not defined` ReferenceError during client bundle factory loading (#285)
108
+ - test: add automated VM execution test for client module loader factory and exports (`test/client-factory-import.test.mjs`)
109
+
110
+ ## 0.8.6 - 2026-09-11
111
+
112
+ - feat(ops): add e2e failover harness covering multi-key pool exhaustion and recovery (#285)
113
+ - feat(ops): add chaos concurrency tests for parallel rotate/cooldown/reset races (#286)
114
+ - feat(ops): persist pool and circuit-breaker state across restarts via `lib/persistence.js` (#287)
115
+ - fix(ops): resolve persistence path from `persistencePath` → `DSH_HOME` → `cwd`; do not read `ctx.baseDir` (#287, #299)
116
+ - feat(ops): sanitize status snapshots — key values never leave the host (#288)
117
+ - feat(ui): accessible modal (dialog role, focus trap, Escape, focus restore) (#289)
118
+ - feat(ui): loading/error/unavailable/empty card states (#290)
119
+ - feat(ui): keyboard navigation and bulk key removal with confirmation (#291)
120
+ - fix(locale): complete en locale coverage for new UI strings (#292)
121
+ - docs: refresh `index.md` coverage matrix and pack policy (#293, #296)
122
+ - docs: document ModuleLoader single-file client constraint and helper split (#294)
123
+ - chore: branch/worktree hygiene audit recorded (#295)
124
+
125
+ ## 0.8.5 - 2026-09-10
126
+
127
+ - feat(ui): add interactive key load distribution bar chart (`.krot-load-chart`, `.krot-load-bar`, `.krot-load-segment`) displaying proportional request volume per key (#283, #284)
128
+ - feat(ui): add modal action confirmation dialog (`.krot-modal`, `.krot-modal-card`) for pool cooldown resets and provider/key deletions (#283, #284)
129
+ - fix(ops): activate jitter (`applyJitter`) on 429 and 5xx backoff calculations in `lib/rotate.js` and `lib/index.js` to eliminate the thundering herd retry spike (#283, #284)
130
+ - fix(ops): synchronously reset provider `circuitBreaker` on `POST /dsh-key-rotation/reset` ops route (#283, #284)
131
+ - test: add dedicated Phase 2 test suite `test/stability-phase2-283.test.mjs` (313 total unit tests passing) (#283, #284)
132
+
133
+ ## 0.8.4 - 2026-09-10
134
+
135
+ - fix(ui): inject `settingsScope` in client manifest and safely guard context property access to prevent ErrorBoundary crash on card mount (#281)
136
+
137
+ ## 0.8.3 - 2026-09-10
138
+
139
+ - feat(ui): unify settings card styling with `dsh-clinebot` design baseline (`.krot-section-card`, `.krot-stat-box`, `.krot-badge-ok/warn/bad`, `.krot-btn-primary/danger`, 36px inputs with brand focus, `data-dsh-plugin="dsh-key-rotation"` style isolation) (#281, #282)
140
+ - feat(ui): add live pool health telemetry stat boxes (configured pools, total keys, healthy/ready keys) (#281)
141
+ - feat(ui): replace hardcoded hex colors with semantic DSH design system tokens (`--dsw-alias-state-*`, `--dsw-alias-bg-*`) for full dark/light theme fidelity (#281)
142
+ - test: add comprehensive stability coverage test suite (`test/stability-coverage-281.test.mjs`, 18 new unit tests covering quota windows, clock monotonicity, sandbox cache, pool network guards, and http bridge helpers) (#281)
143
+
144
+ ## 0.8.2 - 2026-09-10
145
+
146
+ - fix(ui): settings live only as `settings.plugin.item` card — fallback `settings.section` sidebar row removed (#275)
147
+ - fix: resolve DSH services via `ctx.get('llm')` / `sctx.get('settings')` instead of bare context properties that proxy to `undefined` (#275)
148
+ - fix(locale): source strings are English-only (`ctx.locale.register(NS, { en })`); other languages come from host `props.t` / translation plugins (#277, GitHub #1)
149
+ - fix(locale): active locale falls back `ctx.locale` snapshot → first `navigator.languages` entry → `en` (core-aligned) (#277)
150
+ - test: card-only, `ctx.get` and full Cyrillic-in-literal locale gates
151
+
152
+ ## 0.8.1 - 2026-09-09
153
+
154
+ - fix(ui): settings card no longer disappears on open — ErrorBoundary around the section and referentially stable `settingsScope.getSnapshot()` (#273)
155
+
156
+ ## 0.8.0 - 2026-09-09
157
+
158
+ Stability block (#260–#270). Changed in v0.8.0.
159
+
160
+ - feat: per-provider **circuit breaker** (`circuitBreakerEnabled`, threshold/openMs/halfOpenProbes) — fail fast while a provider is down (#260)
161
+ - fix: **monotonic process clock** (`performance.timeOrigin + performance.now`) for cooldowns/durations so NTP jumps do not corrupt remaining times (#261)
162
+ - perf: **BoundedMap** (max + TTL + LRU) foundation for usage/notify maps (#262)
163
+ - fix: **non-blocking webhook notify** via bounded NotifyQueue with backoff — rotate() never awaits webhook I/O (#263)
164
+ - feat: **atomic file I/O helpers** (`atomicWriteFile`/`safeReadJson`/`safeParseJson`) — corrupt JSON never wipes previous state (#264)
165
+ - fix: in-stream failover remains safe across reload; `quotaStore` properly injected into rotate (#265)
166
+ - feat: **clone-route GC** — `expectedClones` computed from live providers; orphans dropped from runtime set (#266)
167
+ - test: **error taxonomy table** — 408/425/429/5xx, ECONNRESET/ETIMEDOUT, gRPC codes classified switch|surface|cooldown (#267)
168
+ - fix: status API returns **single snapshot** with `circuit`, `meta.expectedClones`, `meta.notifyQueue` (#268)
169
+ - test: **smoke harness** `test/smoke-rotation-080.test.mjs` — mock 429 → key switch → success (#269)
170
+ - docs: full 0.8.0 documentation package (README en/ru/zh config tables, DESIGN taxonomy, index test matrix) (#270)
171
+
172
+ ## 0.7.40 - 2026-09-09
173
+
174
+ - docs: align README en/ru/zh and DESIGN.md with shipped surface after v0.7.36 de-bloat (#251)
175
+ - docs: add project `index.md` and `AGENTS.md` (#252)
176
+ - refactor: split `lib/index.js` into `rotate.js`, `http-bridge.js`, `routes-ops.js` (#253)
177
+ - feat: `verboseLogging` config (default false) gates per-request rotation logs (#254)
178
+ - docs: CHANGELOG history and task_plan hygiene (#255)
179
+
180
+ ## 0.7.39 - 2026-08-28
181
+
182
+ - fix(heal): wire `lastUsedAt` into `credentials.resolve` and idle self-heal sweep; cache sweep runtime (issue #249, PR #250)
183
+ - perf: fewer redundant `buildRuntime()` calls on hot path
184
+
185
+ ## 0.7.38 - 2026-08-28
186
+
187
+ - perf(hotpath): single-pass `extractRateLimit`, memoized `todayIso`, zero-allocation status totals, purge stale notify maps (issue #246)
188
+
189
+ ## 0.7.37 - 2026-08-28
190
+
191
+ - fix(concurrency): AsyncLocalStorage request isolation for `pickedRef`; stream-exception failover before first token; copy `attemptList`; wire 30-day usage compaction; clear quarantine after successful sandbox probe (issue #243)
192
+
193
+ ## 0.7.36 - 2026-08-27
194
+
195
+ - refactor: remove overengineered modules (`shadow`, `incident`, `agent-budget`, `region`, `canary`, `maintenance`)
196
+ - perf: memoize `buildRuntime()`, atomic pointer round-robin, HTTP/gRPC error detection, exhaustion ETA, smart visibility polling
197
+
198
+ ## 0.7.34 - 2026-08-27
199
+
200
+ - fix(authoring): lifecycle dispose for credentials.resolve / stream listeners; secret role masking; settingsScope integration (PR #241)
201
+
202
+ ## 0.7.33 - 2026-08-27
203
+
204
+ - fix: key probing baseURL via pool owner; cascade recursion guard; midnight PST sign; timer cleanup via cordis effect; stale lock recovery in least-loaded balancer; zh locale
205
+
206
+ ## 0.7.31 - 2026-08-27
207
+
208
+ - perf: O(1) token bucket; soft vs hard backoff; penalty decay; cooldown jitter; canary probing (later removed in 0.7.36); TTFT percentiles; webhook digest; 30-day usage compaction; optimistic UI + filter pills
209
+
210
+ ## 0.7.20 - 2026-08-27
211
+
212
+ - fix(client): hoist `h` to factory scope for KeyRotationCard (#155)
213
+
214
+ ## Earlier
215
+
216
+ See Gitea release history and README changelog sections for pre-0.7.20 work.
@@ -0,0 +1,123 @@
1
+ // lib/budget-monitor.js — periodic budget, expiry, low health and SLO alerts
2
+ import { expiringSoon, shouldNotifyDaily, costForDay, costForWeek, budgetVerdict } from './pool.js';
3
+
4
+ const DAY_MS = 86400000;
5
+
6
+ export function checkBudgetAndHealthAlerts({
7
+ runtime,
8
+ poolState,
9
+ now,
10
+ expiryNotifiedAt,
11
+ budgetNotifiedAt,
12
+ lowHealthNotifiedAt,
13
+ sloNotifiedAt,
14
+ latencyHistogram,
15
+ webhookSender,
16
+ logger,
17
+ }) {
18
+ try {
19
+ const seen = new Set();
20
+ for (const pool of runtime.poolByRef.values()) {
21
+ if (seen.has(pool.base)) continue;
22
+ seen.add(pool.base);
23
+ // #207: keys expiring within expiryWarnDays -> one webhook per key/day
24
+ for (const { ref, expiresInDays } of expiringSoon(pool, runtime.expiryWarnDays, now)) {
25
+ if (!shouldNotifyDaily(expiryNotifiedAt, pool.base + ':' + ref, now)) continue;
26
+ logger?.warn?.(`[dsh-key-rotation] ${pool.base}: key ${ref} expires in ~${expiresInDays}d`);
27
+ if (runtime.notifyWebhook) {
28
+ webhookSender.send(runtime.notifyWebhook, {
29
+ title: `Key expiring soon: ${pool.base}`,
30
+ text: `${ref} expires in ~${expiresInDays} day(s)`,
31
+ provider: pool.base,
32
+ kind: 'expiry',
33
+ keys: [ref],
34
+ });
35
+ }
36
+ }
37
+ // #208: daily/weekly budget -> warn webhook, optional 1-day pause at 100%
38
+ const budget = runtime.providerBudgets.get(pool.base);
39
+ if (!budget) continue;
40
+ const daily = costForDay(pool.state.costDays);
41
+ const weekly = costForWeek(pool.state.costDays, now);
42
+ const verdict = budgetVerdict(daily, budget.costBudgetDaily);
43
+ const wVerdict = budgetVerdict(weekly, budget.costBudgetWeekly);
44
+ const hit = verdict.warn || wVerdict.warn;
45
+ if (hit && shouldNotifyDaily(budgetNotifiedAt, pool.base + ':budget', now)) {
46
+ logger?.warn?.(`[dsh-key-rotation] ${pool.base}: cost budget - day $${daily.toFixed(2)}/$${budget.costBudgetDaily} week $${weekly.toFixed(2)}/$${budget.costBudgetWeekly}`);
47
+ if (runtime.notifyWebhook) {
48
+ // #217: budget webhook gains action buttons when a callback token is configured
49
+ const token = runtime.webhookActionToken ?? '';
50
+ webhookSender.send(runtime.notifyWebhook, {
51
+ title: `Cost budget: ${pool.base}`,
52
+ text: `day $${daily.toFixed(2)} of $${budget.costBudgetDaily} · week $${weekly.toFixed(2)} of $${budget.costBudgetWeekly}` + (verdict.exceeded || wVerdict.exceeded ? ' · EXCEEDED' : ''),
53
+ provider: pool.base,
54
+ kind: 'budget',
55
+ spend: { daily, weekly },
56
+ actionToken: token || undefined,
57
+ actions: token ? [
58
+ { id: `pause-${pool.base}`, label: 'Pause 1h' },
59
+ { id: `reset-${pool.base}`, label: 'Reset cooldown' },
60
+ ] : undefined,
61
+ });
62
+ }
63
+ }
64
+ if ((verdict.exceeded || wVerdict.exceeded) && budget.pauseOnBudget) {
65
+ const until = now + DAY_MS;
66
+ for (const ref of pool.refs) {
67
+ if ((pool.state.failedUntil.get(ref) ?? 0) < until) pool.state.failedUntil.set(ref, until);
68
+ }
69
+ }
70
+ // #221: pool running low - webhook while healthy < warnBelowHealthy
71
+ const warnBelow = runtime.warnBelowHealthy ?? 0;
72
+ if (warnBelow > 0) {
73
+ let healthy = 0;
74
+ for (const ref of pool.refs) {
75
+ const fu = pool.state.failedUntil.get(ref);
76
+ if (fu !== undefined && fu > now) continue;
77
+ const exp = pool.expiresAt?.[ref];
78
+ if (exp !== undefined && now >= exp) continue;
79
+ healthy++;
80
+ }
81
+ if (healthy < warnBelow && shouldNotifyDaily(lowHealthNotifiedAt, pool.base, now)) {
82
+ logger?.warn?.(`[dsh-key-rotation] ${pool.base}: pool running low - ${healthy}/${pool.refs.length} healthy`);
83
+ if (runtime.notifyWebhook) {
84
+ const token = runtime.webhookActionToken ?? '';
85
+ webhookSender.send(runtime.notifyWebhook, {
86
+ title: `Pool running low: ${pool.base}`,
87
+ text: `${healthy}/${pool.refs.length} keys healthy (alert below ${warnBelow})`,
88
+ provider: pool.base,
89
+ kind: 'low-health',
90
+ healthy,
91
+ total: pool.refs.length,
92
+ actionToken: token || undefined,
93
+ actions: token ? [{ id: `reset-${pool.base}`, label: 'Reset cooldown' }] : undefined,
94
+ });
95
+ }
96
+ }
97
+ }
98
+ // #225: latency SLO - webhook when a key's p95 exceeds the threshold
99
+ const slo = runtime.latencySloMs ?? 0;
100
+ if (slo > 0) {
101
+ for (const ref of pool.refs) {
102
+ const snap = latencyHistogram.snapshot(ref);
103
+ if (!snap.p95 || snap.p95 <= slo) continue;
104
+ if (!shouldNotifyDaily(sloNotifiedAt, pool.base + ':' + ref + ':slo', now)) continue;
105
+ logger?.warn?.(`[dsh-key-rotation] ${pool.base}: ${ref} p95 ${Math.round(snap.p95)}ms > SLO ${slo}ms`);
106
+ if (runtime.notifyWebhook) {
107
+ webhookSender.send(runtime.notifyWebhook, {
108
+ title: `Latency SLO exceeded: ${pool.base}`,
109
+ text: `${ref} p95 ${Math.round(snap.p95)}ms > ${slo}ms (${snap.count} samples)`,
110
+ provider: pool.base,
111
+ kind: 'latency-slo',
112
+ ref,
113
+ p95: Math.round(snap.p95),
114
+ slo,
115
+ });
116
+ }
117
+ }
118
+ }
119
+ }
120
+ } catch (_) {
121
+ /* maintenance must never crash the sweep */
122
+ }
123
+ }
package/lib/client.js CHANGED
@@ -21,8 +21,21 @@ window.__ModuleLoader__.load({
21
21
  const React = require('react');
22
22
  const h = React.createElement;
23
23
 
24
+ // Core primitives and chevron icon
25
+ let ChevronIcon = null;
26
+ try {
27
+ const primitives = require('@deepseek-ai/dsh-client-primitives') || require('@deepseek-ai/dsh-client-icons');
28
+ ChevronIcon = primitives && (primitives.IconChevronDownOutline14 || primitives.IconChevronDownOutline);
29
+ } catch (err) {
30
+ ChevronIcon = null;
31
+ }
32
+
24
33
  const CONFIG_PATH = '/dsh-key-rotation/config';
25
34
  const NS = 'dsh-key-rotation';
35
+ // Plugins page row seat (DSH 0.1.6-alpha.2): key = '<package name>#<row id>'.
36
+ const PKG = '@goodandready/dsh-key-rotation';
37
+ const ROW_ID = 'dsh-key-rotation';
38
+ const ROW_CONFIG_KEY = PKG + '#' + ROW_ID;
26
39
 
27
40
  // -------------------------------------------------------------- i18n
28
41
  const en = {
@@ -413,7 +426,7 @@ window.__ModuleLoader__.load({
413
426
  '.krot-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}',
414
427
  '.krot p{margin:0}',
415
428
  '.krot-hint{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}',
416
- '.krot-err{font-size:13px;color:var(--dsw-alias-state-error-primary);padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 10%, transparent);display:flex;align-items:center;gap:8px}',
429
+ '.krot-err{font-size:13px;color:var(--dsw-alias-state-error-primary);padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);display:flex;align-items:center;gap:8px}',
417
430
  '.krot-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}',
418
431
  '.krot-field{display:flex;flex-direction:column;gap:6px}',
419
432
  '.krot-in{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;height:36px;font-family:inherit;min-width:0;width:100%;box-sizing:border-box;transition:border-color .15s ease}',
@@ -436,15 +449,15 @@ window.__ModuleLoader__.load({
436
449
  '.krot-load-legend{display:flex;flex-wrap:wrap;gap:8px;margin-top:6px;font-size:11px}',
437
450
  '.krot-load-item{display:inline-flex;align-items:center;gap:4px}',
438
451
  '.krot-load-dot{width:7px;height:7px;border-radius:50%}',
439
- '.krot-modal-backdrop{position:fixed;top:0;left:0;right:0;bottom:0;background:var(--dsw-alias-bg-overlay, color-mix(in srgb, var(--dsw-alias-bg-layer-1, #000) 55%, transparent));z-index:9999;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px)}',
440
- '.krot-modal-card{width:90%;max-width:420px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;box-shadow:0 12px 36px color-mix(in srgb, var(--dsw-alias-bg-layer-1, #000) 28%, transparent);padding:20px;display:flex;flex-direction:column;gap:12px}',
452
+ '.krot-modal-backdrop{position:fixed;top:0;left:0;right:0;bottom:0;background:var(--dsw-alias-bg-overlay, color-mix(in srgb, var(--dsw-alias-bg-layer-1) 55%, transparent));z-index:9999;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px)}',
453
+ '.krot-modal-card{width:90%;max-width:420px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;box-shadow:0 12px 36px color-mix(in srgb, var(--dsw-alias-bg-layer-1) 28%, transparent);padding:20px;display:flex;flex-direction:column;gap:12px}',
441
454
  '.krot-modal-title{font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary)}',
442
455
  '.krot-modal-desc{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.45}',
443
456
  '.krot-modal-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:8px}',
444
457
  '.krot-badge{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500;white-space:nowrap}',
445
- '.krot-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary, #10b981) 8%, transparent)}',
446
- '.krot-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary, #f59e0b) 8%, transparent)}',
447
- '.krot-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 8%, transparent)}',
458
+ '.krot-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent)}',
459
+ '.krot-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 8%, transparent)}',
460
+ '.krot-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent)}',
448
461
  '.krot-prov{display:flex;flex-direction:column;gap:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);padding:14px 16px}',
449
462
  '.krot-prov-head{display:flex;gap:10px;align-items:center;flex-wrap:wrap}',
450
463
  '.krot-prov-head select{flex:1;min-width:180px}',
@@ -462,18 +475,18 @@ window.__ModuleLoader__.load({
462
475
  '.krot-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4,var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed,var(--dsw-alias-border-l2))}',
463
476
  '.krot-btn-primary,.krot-save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent;font-weight:600}',
464
477
  '.krot-btn-primary:hover:not(:disabled),.krot-save:hover:not(:disabled){background:var(--dsw-alias-label-primary)!important;color:var(--dsw-alias-bg-layer-3)!important;opacity:0.88}',
465
- '.krot-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 30%, transparent)}',
466
- '.krot-btn-danger:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 12%, transparent)!important;border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 50%, transparent)}',
478
+ '.krot-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 30%, transparent)}',
479
+ '.krot-btn-danger:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)!important;border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 50%, transparent)}',
467
480
  '.krot-btn:disabled{opacity:0.45;cursor:not-allowed}',
468
481
  '.krot-foot{display:flex;gap:10px;align-items:center;flex-wrap:wrap}',
469
482
  '.krot-filter-bar{display:flex;gap:8px;margin:4px 0 10px;flex-wrap:wrap}',
470
483
  '.krot-pill{appearance:none;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:4px 12px;font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary);cursor:pointer;transition:all .15s ease}',
471
484
  '.krot-pill:hover{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}',
472
485
  '.krot-pill-active{background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)!important;font-weight:600}',
473
- '.krot-pill-warn{border-color:color-mix(in srgb, var(--dsw-alias-state-warning-primary, #f59e0b) 30%, transparent);color:var(--dsw-alias-state-warning-primary)}',
474
- '.krot-pill-err{border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 30%, transparent);color:var(--dsw-alias-state-error-primary)}',
475
- '.krot-alert-ok{padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-success-primary, #10b981) 10%, transparent);color:var(--dsw-alias-state-success-primary);font-size:13px;display:flex;align-items:center;gap:10px}',
476
- '.krot-alert-bad{padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 10%, transparent);color:var(--dsw-alias-state-error-primary);font-size:13px;display:flex;align-items:center;gap:10px}',
486
+ '.krot-pill-warn{border-color:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 30%, transparent);color:var(--dsw-alias-state-warning-primary)}',
487
+ '.krot-pill-err{border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 30%, transparent);color:var(--dsw-alias-state-error-primary)}',
488
+ '.krot-alert-ok{padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);color:var(--dsw-alias-state-success-primary);font-size:13px;display:flex;align-items:center;gap:10px}',
489
+ '.krot-alert-bad{padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);color:var(--dsw-alias-state-error-primary);font-size:13px;display:flex;align-items:center;gap:10px}',
477
490
  '.krot-event-stream{display:flex;flex-direction:column;gap:6px;margin-top:8px;padding:10px 12px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:8px}',
478
491
  '.krot-event-row{display:flex;align-items:center;gap:8px;font-size:12px;padding:4px 0;border-bottom:1px solid var(--dsw-alias-border-l1)}',
479
492
  '.krot-event-time{font-family:ui-monospace,Menlo,Consolas,monospace;color:var(--dsw-alias-label-tertiary);font-size:11px}',
@@ -487,7 +500,7 @@ window.__ModuleLoader__.load({
487
500
  '.krot-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:16px 0 8px}',
488
501
  '.krot-header-chip{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:12px;font-weight:600;cursor:pointer;position:relative;user-select:none;transition:all .15s ease}',
489
502
  '.krot-header-chip:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-3));border-color:var(--dsw-alias-border-l1);transform:translateY(-0.5px)}',
490
- '.krot-popover{position:absolute;top:calc(100% + 6px);right:0;z-index:10000;min-width:220px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:12px 14px;box-shadow:0 16px 40px color-mix(in srgb, var(--dsw-alias-bg-layer-1, #000) 45%, transparent),0 2px 8px color-mix(in srgb, var(--dsw-alias-bg-layer-1, #000) 15%, transparent);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);display:flex;flex-direction:column;gap:8px;text-align:left}',
503
+ '.krot-popover{position:absolute;top:calc(100% + 6px);right:0;z-index:10000;min-width:220px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:12px 14px;box-shadow:0 16px 40px color-mix(in srgb, var(--dsw-alias-bg-layer-1) 45%, transparent),0 2px 8px color-mix(in srgb, var(--dsw-alias-bg-layer-1) 15%, transparent);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);display:flex;flex-direction:column;gap:8px;text-align:left}',
491
504
  '.krot-pop-title{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--dsw-alias-label-secondary)}',
492
505
  '.krot-pop-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:4px 0}',
493
506
  '.krot-pop-name{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:8px}',
@@ -565,6 +578,70 @@ window.__ModuleLoader__.load({
565
578
  };
566
579
  }
567
580
 
581
+ function UpdaterSection({ t }) {
582
+ const [state, setState] = React.useState({ phase: 'idle', current: '', latest: '', message: '' });
583
+ const load = React.useCallback(async () => {
584
+ setState((s) => ({ ...s, phase: 'loading', message: '' }));
585
+ try {
586
+ const res = await fetch('/api/dsh-key-rotation/update', { headers: { 'x-dsh-plugin-update': '1' } });
587
+ const data = await res.json();
588
+ if (!res.ok) throw new Error(data?.error?.message || t('updateFailed'));
589
+ setState({
590
+ phase: data.updateAvailable ? 'available' : 'current',
591
+ current: data.currentVersion || '',
592
+ latest: data.latestVersion || '',
593
+ message: data.updateAvailable ? t('updateAvailable') : t('updateNone'),
594
+ });
595
+ } catch (e) {
596
+ setState({ phase: 'error', current: '', latest: '', message: e?.message || t('updateFailed') });
597
+ }
598
+ }, [t]);
599
+ React.useEffect(() => { load(); }, [load]);
600
+ const run = async () => {
601
+ setState((s) => ({ ...s, phase: 'running', message: t('updateRunning') }));
602
+ try {
603
+ const res = await fetch('/api/dsh-key-rotation/update', {
604
+ method: 'POST',
605
+ headers: { 'x-dsh-plugin-update': '1', 'content-type': 'application/json' },
606
+ });
607
+ const data = await res.json();
608
+ if (!res.ok) throw new Error(data?.error?.message || t('updateFailed'));
609
+ setState({
610
+ phase: 'done',
611
+ current: data.currentVersion || '',
612
+ latest: data.installedVersion || data.latestVersion || '',
613
+ message: t('updateRestart'),
614
+ });
615
+ } catch (e) {
616
+ setState({ phase: 'error', current: '', latest: '', message: e?.message || t('updateFailed') });
617
+ }
618
+ };
619
+ return h('div', { className: 'krot-update', style: { marginTop: 12, paddingTop: 12, borderTop: '1px solid var(--dsw-alias-border-l2)' } },
620
+ h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
621
+ h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } },
622
+ t('currentVersion') + ': ' + (state.current || '—')),
623
+ state.latest ? h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } },
624
+ t('latestVersion') + ': ' + state.latest) : null,
625
+ h('button', {
626
+ className: 'krot-btn',
627
+ type: 'button',
628
+ disabled: state.phase === 'running' || state.phase === 'loading',
629
+ onClick: load,
630
+ }, t('updateCheck')),
631
+ state.phase === 'available' ? h('button', {
632
+ className: 'krot-btn krot-btn-primary',
633
+ type: 'button',
634
+ disabled: state.phase === 'running',
635
+ onClick: run,
636
+ }, t('updateRun')) : null,
637
+ ),
638
+ state.message ? h('div', {
639
+ className: state.phase === 'error' ? 'krot-alert-bad' : state.phase === 'done' ? 'krot-alert-ok' : '',
640
+ style: { marginTop: 8, fontSize: 12 },
641
+ }, state.message) : null,
642
+ );
643
+ }
644
+
568
645
  function KeyRotationSection(props) {
569
646
  const t = resolveT(props);
570
647
  const settingsScope = React.useMemo(() => {
@@ -1716,94 +1793,42 @@ window.__ModuleLoader__.load({
1716
1793
  // (settings.plugin.item), matching Model Sync / Spendmeter / Vision Bridge.
1717
1794
  // key MUST equal the settings namespace (NS), else the tab silently skips it.
1718
1795
 
1719
- function UpdaterSection({ t }) {
1720
- const [state, setState] = React.useState({ phase: 'idle', current: '', latest: '', message: '' });
1721
- const load = React.useCallback(async () => {
1722
- setState((s) => ({ ...s, phase: 'loading', message: '' }));
1723
- try {
1724
- const res = await fetch('/api/dsh-key-rotation/update', { headers: { 'x-dsh-plugin-update': '1' } });
1725
- const data = await res.json();
1726
- if (!res.ok) throw new Error(data?.error?.message || t('updateFailed'));
1727
- setState({
1728
- phase: data.updateAvailable ? 'available' : 'current',
1729
- current: data.currentVersion || '',
1730
- latest: data.latestVersion || '',
1731
- message: data.updateAvailable ? t('updateAvailable') : t('updateNone'),
1732
- });
1733
- } catch (e) {
1734
- setState({ phase: 'error', current: '', latest: '', message: e?.message || t('updateFailed') });
1735
- }
1736
- }, [t]);
1737
- React.useEffect(() => { load(); }, [load]);
1738
- const run = async () => {
1739
- setState((s) => ({ ...s, phase: 'running', message: t('updateRunning') }));
1740
- try {
1741
- const res = await fetch('/api/dsh-key-rotation/update', {
1742
- method: 'POST',
1743
- headers: { 'x-dsh-plugin-update': '1', 'content-type': 'application/json' },
1744
- });
1745
- const data = await res.json();
1746
- if (!res.ok) throw new Error(data?.error?.message || t('updateFailed'));
1747
- setState({
1748
- phase: 'done',
1749
- current: data.currentVersion || '',
1750
- latest: data.installedVersion || data.latestVersion || '',
1751
- message: t('updateRestart'),
1752
- });
1753
- } catch (e) {
1754
- setState({ phase: 'error', current: '', latest: '', message: e?.message || t('updateFailed') });
1755
- }
1756
- };
1757
- return h('div', { className: 'krot-update', style: { marginTop: 12, paddingTop: 12, borderTop: '1px solid var(--dsw-alias-border-l2)' } },
1758
- h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
1759
- h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } },
1760
- t('currentVersion') + ': ' + (state.current || '—')),
1761
- state.latest ? h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } },
1762
- t('latestVersion') + ': ' + state.latest) : null,
1763
- h('button', {
1764
- className: 'krot-btn',
1765
- type: 'button',
1766
- disabled: state.phase === 'running' || state.phase === 'loading',
1767
- onClick: load,
1768
- }, t('updateCheck')),
1769
- state.phase === 'available' ? h('button', {
1770
- className: 'krot-btn krot-btn-primary',
1771
- type: 'button',
1772
- disabled: state.phase === 'running',
1773
- onClick: run,
1774
- }, t('updateRun')) : null,
1775
- ),
1776
- state.message ? h('div', {
1777
- className: state.phase === 'error' ? 'krot-alert-bad' : state.phase === 'done' ? 'krot-alert-ok' : '',
1778
- style: { marginTop: 8, fontSize: 12 },
1779
- }, state.message) : null,
1780
- );
1781
- }
1782
-
1783
1796
  function KeyRotationCard(props) {
1784
1797
  const locale = useLocale();
1785
1798
  const t = resolveT(props);
1786
- const [open, setOpen] = React.useState(false);
1787
- return h('div', { className: 'krot-card' + (open ? ' krot-card-open' : '') },
1788
- h('button', { type: 'button', className: 'krot-card-header', 'aria-expanded': open, onClick: () => setOpen((v) => !v) },
1799
+ const page = !!(props && props.view === 'page');
1800
+ const [open, setOpen] = React.useState(!!page);
1801
+ // Row seat (plugins.row.config): the host page draws title/icon/crumb and the
1802
+ // padding, so the summary is a one-liner and the page drops our card chrome.
1803
+ if (props && props.view === 'summary') {
1804
+ return h('span', { className: 'krot-card-description' }, t('subtitle'));
1805
+ }
1806
+ return h(page ? 'div' : 'div', { className: page ? 'krot-page' : 'krot-card' + (open ? ' krot-card-open' : '') },
1807
+ h('button', { type: 'button', className: 'krot-card-header', style: page ? { display: 'none' } : undefined, 'aria-expanded': page ? true : open, onClick: () => setOpen((v) => !v) },
1789
1808
  h('span', { className: 'krot-card-head-text' },
1790
1809
  h('span', { className: 'krot-card-name' }, t('title')),
1791
1810
  h('span', { className: 'krot-card-description' }, t('subtitle'))),
1792
1811
  h('span', { className: 'krot-card-chevron' + (open ? ' krot-card-chevron-open' : ''), 'aria-hidden': 'true' },
1793
1812
  h('svg', { width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, style: { display: 'block' } },
1794
1813
  h('path', { d: 'M3.5 5.25L7 8.75L10.5 5.25' })))),
1795
- open ? h('div', { className: 'krot-card-body' }, h(KeyRotationErrorBoundary, null, h(KeyRotationSection, { ...props, locale }))) : null);
1814
+ (page || open) ? h('div', { className: 'krot-card-body' }, h(KeyRotationErrorBoundary, null, h(KeyRotationSection, { ...props, locale }))) : null);
1796
1815
  }
1797
1816
  // #275: card only — no top-level sidebar row. The host sidebar is a
1798
1817
  // shared flat list; a duplicate row wastes a slot and confuses Settings.
1818
+ // Row seat first (the seat the current core renders), legacy seat after it.
1799
1819
  try {
1820
+ ctx.slots.inject('plugins.row.config', () =>
1821
+ ctx.slots.register(
1822
+ { name: 'plugins.row.config', key: ROW_CONFIG_KEY, locale: NS, inject: () => ({ ctx }) },
1823
+ KeyRotationCard,
1824
+ ));
1800
1825
  ctx.slots.inject('settings.plugin.item', () =>
1801
1826
  ctx.slots.register(
1802
1827
  { name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) },
1803
1828
  KeyRotationCard,
1804
1829
  ));
1805
1830
  } catch (e) {
1806
- bestEffort('slots.register.log', () => { console.error('[dsh-key-rotation] settings.plugin.item register failed', e); });
1831
+ bestEffort('slots.register.log', () => { console.error('[dsh-key-rotation] settings seats register failed', e); });
1807
1832
  }
1808
1833
  }
1809
1834