@goodandready/dsh-key-rotation 0.7.39 → 0.8.0
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 +27 -11
- package/README.ru.md +33 -10
- package/README.zh.md +26 -3
- package/lib/atomic-io.js +60 -0
- package/lib/bounded-map.js +65 -0
- package/lib/circuit-breaker.js +93 -0
- package/lib/clock.js +24 -0
- package/lib/error-taxonomy.js +66 -0
- package/lib/http-bridge.js +169 -0
- package/lib/index.js +97 -944
- package/lib/notify-queue.js +75 -0
- package/lib/pool.js +2 -1
- package/lib/rotate.js +261 -0
- package/lib/routes-ops.js +604 -0
- package/package.json +3 -2
|
@@ -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
|
+
}
|