@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,34 @@
|
|
|
1
|
+
// src/sdk/patch.ts
|
|
2
|
+
export function applyItemPatch(item, patch) {
|
|
3
|
+
const out = JSON.parse(JSON.stringify(item));
|
|
4
|
+
if (patch.name !== undefined)
|
|
5
|
+
out.name = patch.name;
|
|
6
|
+
if (patch.notes !== undefined)
|
|
7
|
+
out.notes = patch.notes;
|
|
8
|
+
if (patch.favorite !== undefined)
|
|
9
|
+
out.favorite = patch.favorite;
|
|
10
|
+
if (patch.folderId !== undefined)
|
|
11
|
+
out.folderId = patch.folderId;
|
|
12
|
+
if (patch.collectionIds !== undefined)
|
|
13
|
+
out.collectionIds = patch.collectionIds;
|
|
14
|
+
if (patch.login) {
|
|
15
|
+
const login = (out.login && typeof out.login === 'object' ? out.login : {});
|
|
16
|
+
if (patch.login.username !== undefined)
|
|
17
|
+
login.username = patch.login.username;
|
|
18
|
+
if (patch.login.password !== undefined)
|
|
19
|
+
login.password = patch.login.password;
|
|
20
|
+
if (patch.login.totp !== undefined)
|
|
21
|
+
login.totp = patch.login.totp;
|
|
22
|
+
if (patch.login.uris !== undefined)
|
|
23
|
+
login.uris = patch.login.uris;
|
|
24
|
+
out.login = login;
|
|
25
|
+
}
|
|
26
|
+
if (patch.fields !== undefined) {
|
|
27
|
+
out.fields = patch.fields.map((f) => ({
|
|
28
|
+
name: f.name,
|
|
29
|
+
value: f.value,
|
|
30
|
+
type: f.hidden ? 1 : 0,
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/sdk/redact.ts
|
|
2
|
+
export const REDACTED = '[REDACTED]';
|
|
3
|
+
function deepClone(obj) {
|
|
4
|
+
return JSON.parse(JSON.stringify(obj));
|
|
5
|
+
}
|
|
6
|
+
function redactFields(fields) {
|
|
7
|
+
return fields.map((f) => {
|
|
8
|
+
if (!f || typeof f !== 'object')
|
|
9
|
+
return f;
|
|
10
|
+
const rec = { ...f };
|
|
11
|
+
const name = typeof rec.name === 'string' ? rec.name : '';
|
|
12
|
+
const hidden = rec.hidden === true || rec.type === 1;
|
|
13
|
+
// Always redact the SSH private key field, even if hidden is false.
|
|
14
|
+
const force = name === 'private_key';
|
|
15
|
+
if ((hidden || force) && typeof rec.value === 'string') {
|
|
16
|
+
rec.value = REDACTED;
|
|
17
|
+
}
|
|
18
|
+
return rec;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function redactItem(item) {
|
|
22
|
+
if (!item || typeof item !== 'object')
|
|
23
|
+
return item;
|
|
24
|
+
const clone = deepClone(item);
|
|
25
|
+
// login.password / login.totp
|
|
26
|
+
if (clone.login && typeof clone.login === 'object') {
|
|
27
|
+
const login = clone.login;
|
|
28
|
+
if (typeof login.password === 'string')
|
|
29
|
+
login.password = REDACTED;
|
|
30
|
+
if (typeof login.totp === 'string')
|
|
31
|
+
login.totp = REDACTED;
|
|
32
|
+
}
|
|
33
|
+
// passwordHistory[].password
|
|
34
|
+
if (Array.isArray(clone.passwordHistory)) {
|
|
35
|
+
clone.passwordHistory = clone.passwordHistory.map((h) => {
|
|
36
|
+
if (!h || typeof h !== 'object')
|
|
37
|
+
return h;
|
|
38
|
+
const rec = { ...h };
|
|
39
|
+
if (typeof rec.password === 'string')
|
|
40
|
+
rec.password = REDACTED;
|
|
41
|
+
return rec;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// card.number / card.code
|
|
45
|
+
if (clone.card && typeof clone.card === 'object') {
|
|
46
|
+
const card = clone.card;
|
|
47
|
+
if (typeof card.number === 'string')
|
|
48
|
+
card.number = REDACTED;
|
|
49
|
+
if (typeof card.code === 'string')
|
|
50
|
+
card.code = REDACTED;
|
|
51
|
+
}
|
|
52
|
+
// identity fields that are typically sensitive
|
|
53
|
+
if (clone.identity && typeof clone.identity === 'object') {
|
|
54
|
+
const identity = clone.identity;
|
|
55
|
+
for (const k of ['ssn', 'passportNumber', 'licenseNumber']) {
|
|
56
|
+
if (typeof identity[k] === 'string')
|
|
57
|
+
identity[k] = REDACTED;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// top-level fields
|
|
61
|
+
if (Array.isArray(clone.fields)) {
|
|
62
|
+
clone.fields = redactFields(clone.fields);
|
|
63
|
+
}
|
|
64
|
+
// attachments often include a signed download URL token; redact it by default.
|
|
65
|
+
if (Array.isArray(clone.attachments)) {
|
|
66
|
+
clone.attachments = clone.attachments.map((a) => {
|
|
67
|
+
if (!a || typeof a !== 'object')
|
|
68
|
+
return a;
|
|
69
|
+
const rec = { ...a };
|
|
70
|
+
if (typeof rec.url === 'string')
|
|
71
|
+
rec.url = REDACTED;
|
|
72
|
+
return rec;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return clone;
|
|
76
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { randomInt } from 'node:crypto';
|
|
2
|
+
const DEFAULT_DEPS = {
|
|
3
|
+
randInt: (max) => randomInt(max),
|
|
4
|
+
};
|
|
5
|
+
const ONSETS = [
|
|
6
|
+
'b',
|
|
7
|
+
'c',
|
|
8
|
+
'd',
|
|
9
|
+
'f',
|
|
10
|
+
'g',
|
|
11
|
+
'h',
|
|
12
|
+
'j',
|
|
13
|
+
'k',
|
|
14
|
+
'l',
|
|
15
|
+
'm',
|
|
16
|
+
'n',
|
|
17
|
+
'p',
|
|
18
|
+
'r',
|
|
19
|
+
's',
|
|
20
|
+
't',
|
|
21
|
+
'v',
|
|
22
|
+
'w',
|
|
23
|
+
'z',
|
|
24
|
+
'br',
|
|
25
|
+
'cr',
|
|
26
|
+
'dr',
|
|
27
|
+
'fr',
|
|
28
|
+
'gr',
|
|
29
|
+
'pr',
|
|
30
|
+
'tr',
|
|
31
|
+
'ch',
|
|
32
|
+
'sh',
|
|
33
|
+
'st',
|
|
34
|
+
'sl',
|
|
35
|
+
'pl',
|
|
36
|
+
];
|
|
37
|
+
const VOWELS = [
|
|
38
|
+
'a',
|
|
39
|
+
'e',
|
|
40
|
+
'i',
|
|
41
|
+
'o',
|
|
42
|
+
'u',
|
|
43
|
+
'ae',
|
|
44
|
+
'ai',
|
|
45
|
+
'ea',
|
|
46
|
+
'ee',
|
|
47
|
+
'ie',
|
|
48
|
+
'oa',
|
|
49
|
+
'oo',
|
|
50
|
+
'ou',
|
|
51
|
+
];
|
|
52
|
+
const CODAS = [
|
|
53
|
+
'',
|
|
54
|
+
'n',
|
|
55
|
+
'r',
|
|
56
|
+
's',
|
|
57
|
+
't',
|
|
58
|
+
'l',
|
|
59
|
+
'm',
|
|
60
|
+
'nd',
|
|
61
|
+
'st',
|
|
62
|
+
'rt',
|
|
63
|
+
'ng',
|
|
64
|
+
];
|
|
65
|
+
function titleCase(s) {
|
|
66
|
+
if (!s)
|
|
67
|
+
return s;
|
|
68
|
+
const first = s.charAt(0);
|
|
69
|
+
return first.toUpperCase() + s.slice(1);
|
|
70
|
+
}
|
|
71
|
+
function randomWord(opts, deps) {
|
|
72
|
+
// "Word-like" usernames without pulling a large word list dependency.
|
|
73
|
+
// Produces a pronounceable-ish token such as "cravon" or "Plenast7".
|
|
74
|
+
for (let i = 0; i < 12; i++) {
|
|
75
|
+
const syllables = 2 + deps.randInt(2); // 2-3
|
|
76
|
+
let s = '';
|
|
77
|
+
for (let j = 0; j < syllables; j++) {
|
|
78
|
+
s += ONSETS[deps.randInt(ONSETS.length)] ?? 'k';
|
|
79
|
+
s += VOWELS[deps.randInt(VOWELS.length)] ?? 'a';
|
|
80
|
+
const coda = CODAS[deps.randInt(CODAS.length)] ?? '';
|
|
81
|
+
// Avoid overly long tokens by preferring empty coda on earlier syllables.
|
|
82
|
+
s += j === syllables - 1 ? coda : coda.length > 1 ? '' : coda;
|
|
83
|
+
}
|
|
84
|
+
if (s.length < 4 || s.length > 18)
|
|
85
|
+
continue;
|
|
86
|
+
if (opts.capitalize)
|
|
87
|
+
s = titleCase(s);
|
|
88
|
+
if (opts.includeNumber)
|
|
89
|
+
s += String(deps.randInt(10));
|
|
90
|
+
return s;
|
|
91
|
+
}
|
|
92
|
+
// Fallback: still deterministic with deps.randInt in tests.
|
|
93
|
+
const fallback = `user${deps.randInt(1_000_000)}`;
|
|
94
|
+
return opts.capitalize ? titleCase(fallback) : fallback;
|
|
95
|
+
}
|
|
96
|
+
function parseEmail(email) {
|
|
97
|
+
const trimmed = email.trim();
|
|
98
|
+
const at = trimmed.indexOf('@');
|
|
99
|
+
if (at <= 0 || at === trimmed.length - 1) {
|
|
100
|
+
throw new Error('Invalid email address');
|
|
101
|
+
}
|
|
102
|
+
const local = trimmed.slice(0, at);
|
|
103
|
+
const domain = trimmed.slice(at + 1);
|
|
104
|
+
return { local, domain };
|
|
105
|
+
}
|
|
106
|
+
function normalizeDomain(domain) {
|
|
107
|
+
const d = domain.trim().replace(/^@+/, '');
|
|
108
|
+
if (!d || d.includes(' ') || d.includes('/')) {
|
|
109
|
+
throw new Error('Invalid domain');
|
|
110
|
+
}
|
|
111
|
+
return d;
|
|
112
|
+
}
|
|
113
|
+
export function generateUsername(input = {}, deps) {
|
|
114
|
+
const d = { ...DEFAULT_DEPS, ...deps };
|
|
115
|
+
const type = input.type ?? 'random_word';
|
|
116
|
+
if (type === 'forwarded_email_alias') {
|
|
117
|
+
// Bitwarden UI can generate email aliases via provider integrations (e.g.
|
|
118
|
+
// SimpleLogin/AnonAddy/etc). The `bw` CLI doesn't expose this today.
|
|
119
|
+
throw new Error('forwarded_email_alias is not supported');
|
|
120
|
+
}
|
|
121
|
+
const word = randomWord({ capitalize: input.capitalize, includeNumber: input.includeNumber }, d);
|
|
122
|
+
if (type === 'random_word')
|
|
123
|
+
return word;
|
|
124
|
+
if (type === 'plus_addressed_email') {
|
|
125
|
+
if (!input.email) {
|
|
126
|
+
throw new Error('email is required for plus_addressed_email');
|
|
127
|
+
}
|
|
128
|
+
const { local, domain } = parseEmail(input.email);
|
|
129
|
+
const baseLocal = local.split('+')[0] ?? local;
|
|
130
|
+
return `${baseLocal}+${word}@${domain}`;
|
|
131
|
+
}
|
|
132
|
+
if (type === 'catch_all_email') {
|
|
133
|
+
if (!input.domain) {
|
|
134
|
+
throw new Error('domain is required for catch_all_email');
|
|
135
|
+
}
|
|
136
|
+
const domain = normalizeDomain(input.domain);
|
|
137
|
+
return `${word}@${domain}`;
|
|
138
|
+
}
|
|
139
|
+
// Exhaustive check.
|
|
140
|
+
const _never = type;
|
|
141
|
+
return _never;
|
|
142
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { createKeychainApp } from './app.js';
|
|
4
|
+
import { runStdioTransport } from './transports/stdio.js';
|
|
5
|
+
const { values } = parseArgs({
|
|
6
|
+
options: {
|
|
7
|
+
stdio: { type: 'boolean', default: false },
|
|
8
|
+
http: { type: 'boolean', default: false },
|
|
9
|
+
},
|
|
10
|
+
strict: false,
|
|
11
|
+
});
|
|
12
|
+
const useStdio = values.stdio === true || process.env.WARDEN_MCP_STDIO === 'true';
|
|
13
|
+
if (useStdio) {
|
|
14
|
+
await runStdioTransport();
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
const PORT = Number.parseInt(process.env.PORT ?? '3005', 10);
|
|
18
|
+
const app = createKeychainApp();
|
|
19
|
+
app.listen(PORT, () => {
|
|
20
|
+
console.log(`[warden-mcp] listening on http://localhost:${PORT}/sse`);
|
|
21
|
+
});
|
|
22
|
+
}
|