@icoretech/warden-mcp 0.1.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/LICENSE +21 -0
- package/README.md +156 -0
- package/bin/warden-mcp.js +60 -0
- package/dist/app.js +3 -0
- package/dist/bw/bwCli.js +106 -0
- package/dist/bw/bwHeaders.js +87 -0
- package/dist/bw/bwPool.js +54 -0
- package/dist/bw/bwSession.js +230 -0
- package/dist/bw/mutex.js +19 -0
- package/dist/sdk/generateArgs.js +64 -0
- package/dist/sdk/keychainSdk.js +1225 -0
- package/dist/sdk/patch.js +34 -0
- package/dist/sdk/redact.js +76 -0
- package/dist/sdk/types.js +2 -0
- package/dist/sdk/usernameGenerator.js +142 -0
- package/dist/server.js +22 -0
- package/dist/tools/registerTools.js +1250 -0
- package/dist/transports/http.js +376 -0
- package/dist/transports/stdio.js +33 -0
- package/package.json +52 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// src/bw/bwSession.ts
|
|
2
|
+
import { rm } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { runBw } from './bwCli.js';
|
|
5
|
+
import { Mutex } from './mutex.js';
|
|
6
|
+
function requiredEnv(name) {
|
|
7
|
+
const v = process.env[name];
|
|
8
|
+
if (!v)
|
|
9
|
+
throw new Error(`Missing required env var: ${name}`);
|
|
10
|
+
return v;
|
|
11
|
+
}
|
|
12
|
+
export function readBwEnv() {
|
|
13
|
+
const unlockIntervalSecondsRaw = process.env.BW_UNLOCK_INTERVAL ?? '300';
|
|
14
|
+
const unlockIntervalSeconds = Number.parseInt(unlockIntervalSecondsRaw, 10);
|
|
15
|
+
const host = requiredEnv('BW_HOST');
|
|
16
|
+
const password = requiredEnv('BW_PASSWORD');
|
|
17
|
+
const clientId = process.env.BW_CLIENTID;
|
|
18
|
+
const clientSecret = process.env.BW_CLIENTSECRET;
|
|
19
|
+
const user = process.env.BW_USER ?? process.env.BW_USERNAME;
|
|
20
|
+
const login = clientId && clientSecret
|
|
21
|
+
? { method: 'apikey', clientId, clientSecret }
|
|
22
|
+
: user
|
|
23
|
+
? { method: 'userpass', user }
|
|
24
|
+
: (() => {
|
|
25
|
+
throw new Error('Missing login env: set BW_CLIENTID+BW_CLIENTSECRET or BW_USER/BW_USERNAME');
|
|
26
|
+
})();
|
|
27
|
+
return {
|
|
28
|
+
host,
|
|
29
|
+
password,
|
|
30
|
+
unlockIntervalSeconds: Number.isFinite(unlockIntervalSeconds)
|
|
31
|
+
? unlockIntervalSeconds
|
|
32
|
+
: 300,
|
|
33
|
+
login,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export class BwSessionManager {
|
|
37
|
+
env;
|
|
38
|
+
lock = new Mutex();
|
|
39
|
+
session = null;
|
|
40
|
+
templateItem = null;
|
|
41
|
+
keepaliveTimer = null;
|
|
42
|
+
configuredHost = null;
|
|
43
|
+
homeDir;
|
|
44
|
+
constructor(env) {
|
|
45
|
+
this.env = env;
|
|
46
|
+
this.homeDir = env.homeDir ?? process.env.HOME ?? '/data';
|
|
47
|
+
}
|
|
48
|
+
baseEnv(extra) {
|
|
49
|
+
return { HOME: this.homeDir, ...(extra ?? {}) };
|
|
50
|
+
}
|
|
51
|
+
// Must be called while holding `this.lock` (i.e. from within `withSession`).
|
|
52
|
+
async getTemplateItemLocked(session) {
|
|
53
|
+
if (this.templateItem)
|
|
54
|
+
return this.templateItem;
|
|
55
|
+
const { stdout } = await runBw(['--session', session, 'get', 'template', 'item'], {
|
|
56
|
+
env: this.baseEnv(),
|
|
57
|
+
timeoutMs: 60_000,
|
|
58
|
+
});
|
|
59
|
+
const parsed = JSON.parse(stdout);
|
|
60
|
+
this.templateItem = parsed;
|
|
61
|
+
return parsed;
|
|
62
|
+
}
|
|
63
|
+
startKeepUnlocked() {
|
|
64
|
+
if (this.keepaliveTimer)
|
|
65
|
+
return;
|
|
66
|
+
const intervalMs = Math.max(10, this.env.unlockIntervalSeconds) * 1000;
|
|
67
|
+
this.keepaliveTimer = setInterval(() => {
|
|
68
|
+
void this.lock.runExclusive(async () => {
|
|
69
|
+
try {
|
|
70
|
+
await this.ensureUnlockedInternal();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Keepalive is best-effort; tools will surface failures.
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}, intervalMs);
|
|
77
|
+
this.keepaliveTimer.unref?.();
|
|
78
|
+
}
|
|
79
|
+
async withSession(fn) {
|
|
80
|
+
return this.lock.runExclusive(async () => {
|
|
81
|
+
const session = await this.ensureUnlockedInternal();
|
|
82
|
+
return fn(session);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async getTemplateItem() {
|
|
86
|
+
return this.lock.runExclusive(async () => {
|
|
87
|
+
const session = await this.ensureUnlockedInternal();
|
|
88
|
+
return this.getTemplateItemLocked(session);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// Use this inside a `withSession` callback to avoid deadlocking by re-taking the same mutex.
|
|
92
|
+
async getTemplateItemForSession(session) {
|
|
93
|
+
return this.getTemplateItemLocked(session);
|
|
94
|
+
}
|
|
95
|
+
async status() {
|
|
96
|
+
return this.withSession(async (session) => {
|
|
97
|
+
const { stdout } = await runBw(['--session', session, 'status'], {
|
|
98
|
+
env: this.baseEnv(),
|
|
99
|
+
timeoutMs: 60_000,
|
|
100
|
+
});
|
|
101
|
+
const parsed = JSON.parse(stdout);
|
|
102
|
+
const serverUrl = typeof parsed.serverUrl === 'string' ? parsed.serverUrl : this.env.host;
|
|
103
|
+
const userEmail = typeof parsed.userEmail === 'string'
|
|
104
|
+
? parsed.userEmail
|
|
105
|
+
: this.env.login.method === 'userpass'
|
|
106
|
+
? this.env.login.user
|
|
107
|
+
: null;
|
|
108
|
+
const summaryParts = ['Vault access ready'];
|
|
109
|
+
if (userEmail)
|
|
110
|
+
summaryParts.push(`for ${userEmail}`);
|
|
111
|
+
if (serverUrl)
|
|
112
|
+
summaryParts.push(`on ${serverUrl}`);
|
|
113
|
+
return {
|
|
114
|
+
...parsed,
|
|
115
|
+
summary: `${summaryParts.join(' ')}.`,
|
|
116
|
+
operational: {
|
|
117
|
+
ready: true,
|
|
118
|
+
sessionValid: true,
|
|
119
|
+
source: 'session_manager',
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// Run a bw command within an existing session, using this manager's HOME/profile.
|
|
125
|
+
// Intended to be used from inside `withSession` to avoid relocking.
|
|
126
|
+
async runForSession(session, args, opts = {}) {
|
|
127
|
+
return runBw(['--session', session, ...args], {
|
|
128
|
+
...opts,
|
|
129
|
+
env: this.baseEnv(opts.env),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async ensureUnlockedInternal() {
|
|
133
|
+
// Ensure server config points to BW_HOST.
|
|
134
|
+
await this.ensureServerConfigured();
|
|
135
|
+
// If we already have a session, check if it still works.
|
|
136
|
+
if (this.session) {
|
|
137
|
+
try {
|
|
138
|
+
const { stdout } = await runBw(['--session', this.session, 'unlock', '--check'], {
|
|
139
|
+
env: this.baseEnv(),
|
|
140
|
+
timeoutMs: 30_000,
|
|
141
|
+
});
|
|
142
|
+
// unlock --check prints "Vault is unlocked!" or similar; exit code 0 means ok.
|
|
143
|
+
void stdout;
|
|
144
|
+
return this.session;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
this.session = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const unlockEnv = this.baseEnv({
|
|
151
|
+
BW_PASSWORD: this.env.password,
|
|
152
|
+
BW_HOST: this.env.host,
|
|
153
|
+
});
|
|
154
|
+
const tryUnlock = async () => {
|
|
155
|
+
try {
|
|
156
|
+
const { stdout } = await runBw(['unlock', '--passwordenv', 'BW_PASSWORD', '--raw'], { env: unlockEnv, timeoutMs: 60_000 });
|
|
157
|
+
return stdout.trim();
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return '';
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const tryLoginRaw = async () => {
|
|
164
|
+
try {
|
|
165
|
+
if (this.env.login.method === 'apikey') {
|
|
166
|
+
const { stdout } = await runBw(['login', '--apikey', '--raw'], {
|
|
167
|
+
env: this.baseEnv({
|
|
168
|
+
BW_CLIENTID: this.env.login.clientId,
|
|
169
|
+
BW_CLIENTSECRET: this.env.login.clientSecret,
|
|
170
|
+
BW_HOST: this.env.host,
|
|
171
|
+
}),
|
|
172
|
+
timeoutMs: 60_000,
|
|
173
|
+
});
|
|
174
|
+
return stdout.trim();
|
|
175
|
+
}
|
|
176
|
+
const { stdout } = await runBw([
|
|
177
|
+
'login',
|
|
178
|
+
this.env.login.user,
|
|
179
|
+
'--passwordenv',
|
|
180
|
+
'BW_PASSWORD',
|
|
181
|
+
'--raw',
|
|
182
|
+
], { env: unlockEnv, timeoutMs: 60_000 });
|
|
183
|
+
return stdout.trim();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return '';
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
// Prefer unlocking first (works when already logged in). If it yields an empty
|
|
190
|
+
// stdout on exit=0 (observed in some bw builds), fall back to login --raw.
|
|
191
|
+
let session = await tryUnlock();
|
|
192
|
+
if (!session)
|
|
193
|
+
session = await tryLoginRaw();
|
|
194
|
+
if (!session)
|
|
195
|
+
session = await tryUnlock();
|
|
196
|
+
if (!session)
|
|
197
|
+
throw new Error('bw login/unlock returned an empty session');
|
|
198
|
+
this.session = session;
|
|
199
|
+
return session;
|
|
200
|
+
}
|
|
201
|
+
async ensureServerConfigured() {
|
|
202
|
+
if (this.configuredHost === this.env.host)
|
|
203
|
+
return;
|
|
204
|
+
// bw requires logout before config server update.
|
|
205
|
+
await runBw(['logout'], { env: this.baseEnv(), timeoutMs: 30_000 }).catch(() => { });
|
|
206
|
+
try {
|
|
207
|
+
await runBw(['config', 'server', this.env.host], {
|
|
208
|
+
env: this.baseEnv(),
|
|
209
|
+
timeoutMs: 30_000,
|
|
210
|
+
});
|
|
211
|
+
this.configuredHost = this.env.host;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// If the CLI data is corrupt/out-of-sync, wiping config is the fastest recovery.
|
|
216
|
+
}
|
|
217
|
+
const home = this.homeDir;
|
|
218
|
+
await rm(join(home, '.config', 'Bitwarden CLI', 'data.json'), {
|
|
219
|
+
force: true,
|
|
220
|
+
}).catch(() => { });
|
|
221
|
+
await rm(join(home, '.config', 'Bitwarden CLI', 'config.json'), {
|
|
222
|
+
force: true,
|
|
223
|
+
}).catch(() => { });
|
|
224
|
+
await runBw(['config', 'server', this.env.host], {
|
|
225
|
+
env: this.baseEnv(),
|
|
226
|
+
timeoutMs: 30_000,
|
|
227
|
+
});
|
|
228
|
+
this.configuredHost = this.env.host;
|
|
229
|
+
}
|
|
230
|
+
}
|
package/dist/bw/mutex.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/bw/mutex.ts
|
|
2
|
+
export class Mutex {
|
|
3
|
+
current = Promise.resolve();
|
|
4
|
+
async runExclusive(fn) {
|
|
5
|
+
let release;
|
|
6
|
+
const next = new Promise((resolve) => {
|
|
7
|
+
release = resolve;
|
|
8
|
+
});
|
|
9
|
+
const prev = this.current;
|
|
10
|
+
this.current = this.current.then(() => next);
|
|
11
|
+
await prev;
|
|
12
|
+
try {
|
|
13
|
+
return await fn();
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
release?.();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
function hasOwn(o, k) {
|
|
2
|
+
return Object.hasOwn(o, k);
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Build args for `bw --raw generate ...`.
|
|
6
|
+
*
|
|
7
|
+
* Defaults:
|
|
8
|
+
* - If the caller does not specify any of {uppercase, lowercase, number, special},
|
|
9
|
+
* we do NOT pass charset flags and let `bw` use its defaults (`-uln --length 14`).
|
|
10
|
+
* - If the caller specifies any charset flag, we treat unspecified flags as
|
|
11
|
+
* "UI defaults": uppercase/lowercase/number default to true; special defaults
|
|
12
|
+
* to false. This makes `{ special: true }` behave like "toggle special on".
|
|
13
|
+
*
|
|
14
|
+
* Note: `bw` doesn’t support explicit "disable numbers"; to exclude a charset you
|
|
15
|
+
* omit that flag. We avoid accidentally triggering `bw` defaults by including
|
|
16
|
+
* at least one charset flag when the user is in "explicit charset" mode.
|
|
17
|
+
*/
|
|
18
|
+
export function buildBwGenerateArgs(input) {
|
|
19
|
+
const args = ['--raw', 'generate'];
|
|
20
|
+
if (input.passphrase) {
|
|
21
|
+
args.push('--passphrase');
|
|
22
|
+
if (typeof input.words === 'number')
|
|
23
|
+
args.push('--words', String(input.words));
|
|
24
|
+
if (typeof input.separator === 'string')
|
|
25
|
+
args.push('--separator', input.separator);
|
|
26
|
+
if (input.capitalize)
|
|
27
|
+
args.push('--capitalize');
|
|
28
|
+
if (input.includeNumber)
|
|
29
|
+
args.push('--includeNumber');
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const hasUpper = hasOwn(input, 'uppercase');
|
|
33
|
+
const hasLower = hasOwn(input, 'lowercase');
|
|
34
|
+
const hasNumber = hasOwn(input, 'number');
|
|
35
|
+
const hasSpecial = hasOwn(input, 'special');
|
|
36
|
+
const explicitCharset = hasUpper || hasLower || hasNumber || hasSpecial;
|
|
37
|
+
if (explicitCharset) {
|
|
38
|
+
const uppercase = hasUpper ? input.uppercase === true : true;
|
|
39
|
+
const lowercase = hasLower ? input.lowercase === true : true;
|
|
40
|
+
const number = hasNumber ? input.number === true : true;
|
|
41
|
+
const special = hasSpecial ? input.special === true : false;
|
|
42
|
+
if (!uppercase && !lowercase && !number && !special) {
|
|
43
|
+
throw new Error('At least one of uppercase/lowercase/number/special must be true');
|
|
44
|
+
}
|
|
45
|
+
if (uppercase)
|
|
46
|
+
args.push('--uppercase');
|
|
47
|
+
if (lowercase)
|
|
48
|
+
args.push('--lowercase');
|
|
49
|
+
if (number)
|
|
50
|
+
args.push('--number');
|
|
51
|
+
if (special)
|
|
52
|
+
args.push('--special');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (typeof input.length === 'number')
|
|
56
|
+
args.push('--length', String(input.length));
|
|
57
|
+
if (typeof input.minNumber === 'number')
|
|
58
|
+
args.push('--minNumber', String(input.minNumber));
|
|
59
|
+
if (typeof input.minSpecial === 'number')
|
|
60
|
+
args.push('--minSpecial', String(input.minSpecial));
|
|
61
|
+
if (input.ambiguous)
|
|
62
|
+
args.push('--ambiguous');
|
|
63
|
+
return args;
|
|
64
|
+
}
|