@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,1225 @@
|
|
|
1
|
+
// src/sdk/keychainSdk.ts
|
|
2
|
+
import { mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
import { BwCliError } from '../bw/bwCli.js';
|
|
6
|
+
import { buildBwGenerateArgs } from './generateArgs.js';
|
|
7
|
+
import { applyItemPatch } from './patch.js';
|
|
8
|
+
import { redactItem } from './redact.js';
|
|
9
|
+
import { generateUsername } from './usernameGenerator.js';
|
|
10
|
+
const ITEM_TYPE = {
|
|
11
|
+
login: 1,
|
|
12
|
+
note: 2,
|
|
13
|
+
card: 3,
|
|
14
|
+
identity: 4,
|
|
15
|
+
};
|
|
16
|
+
const URI_MATCH = {
|
|
17
|
+
domain: 0,
|
|
18
|
+
host: 1,
|
|
19
|
+
startsWith: 2,
|
|
20
|
+
exact: 3,
|
|
21
|
+
regex: 4,
|
|
22
|
+
never: 5,
|
|
23
|
+
};
|
|
24
|
+
const URI_MATCH_REVERSE = {
|
|
25
|
+
0: 'domain',
|
|
26
|
+
1: 'host',
|
|
27
|
+
2: 'startsWith',
|
|
28
|
+
3: 'exact',
|
|
29
|
+
4: 'regex',
|
|
30
|
+
5: 'never',
|
|
31
|
+
};
|
|
32
|
+
function deepClone(obj) {
|
|
33
|
+
return JSON.parse(JSON.stringify(obj));
|
|
34
|
+
}
|
|
35
|
+
function encodeJsonForBw(value) {
|
|
36
|
+
return Buffer.from(JSON.stringify(value), 'utf8').toString('base64');
|
|
37
|
+
}
|
|
38
|
+
function normalizeFields(fields) {
|
|
39
|
+
if (!fields)
|
|
40
|
+
return undefined;
|
|
41
|
+
return fields.map((f) => ({
|
|
42
|
+
name: f.name,
|
|
43
|
+
value: f.value,
|
|
44
|
+
// Bitwarden uses numeric "type" for custom fields:
|
|
45
|
+
// 0 = text, 1 = hidden.
|
|
46
|
+
type: f.hidden ? 1 : 0,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
function normalizeUris(uris) {
|
|
50
|
+
if (!uris)
|
|
51
|
+
return undefined;
|
|
52
|
+
return uris.map((u) => ({
|
|
53
|
+
uri: u.uri,
|
|
54
|
+
match: u.match ? URI_MATCH[u.match] : null,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
function denormalizeUris(raw) {
|
|
58
|
+
if (!Array.isArray(raw))
|
|
59
|
+
return undefined;
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const u of raw) {
|
|
62
|
+
if (!u || typeof u !== 'object')
|
|
63
|
+
continue;
|
|
64
|
+
const rec = u;
|
|
65
|
+
const uri = rec.uri;
|
|
66
|
+
if (typeof uri !== 'string' || uri.length === 0)
|
|
67
|
+
continue;
|
|
68
|
+
const match = typeof rec.match === 'number' ? URI_MATCH_REVERSE[rec.match] : undefined;
|
|
69
|
+
out.push({ uri, match });
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function kindFromItem(item) {
|
|
74
|
+
const type = item.type;
|
|
75
|
+
if (type === ITEM_TYPE.login)
|
|
76
|
+
return 'login';
|
|
77
|
+
if (type === ITEM_TYPE.card)
|
|
78
|
+
return 'card';
|
|
79
|
+
if (type === ITEM_TYPE.identity)
|
|
80
|
+
return 'identity';
|
|
81
|
+
if (type === ITEM_TYPE.note)
|
|
82
|
+
return isSshKeyItem(item) ? 'ssh_key' : 'note';
|
|
83
|
+
return 'note';
|
|
84
|
+
}
|
|
85
|
+
function isSshKeyItem(item) {
|
|
86
|
+
if (item.type !== ITEM_TYPE.note)
|
|
87
|
+
return false;
|
|
88
|
+
const fields = item.fields;
|
|
89
|
+
if (!Array.isArray(fields))
|
|
90
|
+
return false;
|
|
91
|
+
const names = new Set(fields
|
|
92
|
+
.map((f) => (f && typeof f === 'object' ? f.name : null))
|
|
93
|
+
.filter((n) => typeof n === 'string'));
|
|
94
|
+
return names.has('public_key') && names.has('private_key');
|
|
95
|
+
}
|
|
96
|
+
export class KeychainSdk {
|
|
97
|
+
bw;
|
|
98
|
+
constructor(bw) {
|
|
99
|
+
this.bw = bw;
|
|
100
|
+
}
|
|
101
|
+
async createLoginForSession(session, input) {
|
|
102
|
+
const tpl = (await this.bw.getTemplateItemForSession(session));
|
|
103
|
+
const item = deepClone(tpl);
|
|
104
|
+
item.type = ITEM_TYPE.login;
|
|
105
|
+
item.name = input.name;
|
|
106
|
+
item.notes = input.notes ?? '';
|
|
107
|
+
item.favorite = input.favorite ?? false;
|
|
108
|
+
if (input.organizationId)
|
|
109
|
+
item.organizationId = input.organizationId;
|
|
110
|
+
if (input.folderId)
|
|
111
|
+
item.folderId = input.folderId;
|
|
112
|
+
item.fields = normalizeFields(input.fields) ?? [];
|
|
113
|
+
const login = (item.login && typeof item.login === 'object'
|
|
114
|
+
? item.login
|
|
115
|
+
: {});
|
|
116
|
+
if (input.username !== undefined)
|
|
117
|
+
login.username = input.username;
|
|
118
|
+
if (input.password !== undefined)
|
|
119
|
+
login.password = input.password;
|
|
120
|
+
if (input.totp !== undefined)
|
|
121
|
+
login.totp = input.totp;
|
|
122
|
+
if (input.uris !== undefined)
|
|
123
|
+
login.uris = normalizeUris(input.uris);
|
|
124
|
+
item.login = login;
|
|
125
|
+
// Set collectionIds optimistically; we'll also enforce with item-collections edit.
|
|
126
|
+
if (input.collectionIds)
|
|
127
|
+
item.collectionIds = input.collectionIds;
|
|
128
|
+
const encoded = encodeJsonForBw(item);
|
|
129
|
+
const { stdout } = await this.bw.runForSession(session, ['create', 'item', encoded], { timeoutMs: 120_000 });
|
|
130
|
+
const created = JSON.parse(stdout);
|
|
131
|
+
if (input.attachments?.length) {
|
|
132
|
+
const dir = await mkdtemp(join(tmpdir(), 'keychain-attach-'));
|
|
133
|
+
try {
|
|
134
|
+
for (const att of input.attachments) {
|
|
135
|
+
const safeBase = basename(att.filename || 'attachment.bin').replace(/[^A-Za-z0-9._-]+/g, '_');
|
|
136
|
+
const safeName = safeBase.length > 0 ? safeBase : 'attachment.bin';
|
|
137
|
+
const p = join(dir, safeName);
|
|
138
|
+
await writeFile(p, Buffer.from(att.contentBase64, 'base64'));
|
|
139
|
+
const { stdout: attOut } = await this.bw.runForSession(session, [
|
|
140
|
+
'create',
|
|
141
|
+
'attachment',
|
|
142
|
+
'--file',
|
|
143
|
+
p,
|
|
144
|
+
'--itemid',
|
|
145
|
+
String(created.id),
|
|
146
|
+
], { timeoutMs: 120_000 });
|
|
147
|
+
// IMPORTANT: bw may return the full item JSON (including secrets) here.
|
|
148
|
+
// Never parse or include it in our response.
|
|
149
|
+
void attOut;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
await rm(dir, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (input.collectionIds?.length) {
|
|
157
|
+
const encodedCols = encodeJsonForBw(input.collectionIds);
|
|
158
|
+
await this.bw
|
|
159
|
+
.runForSession(session, ['edit', 'item-collections', String(created.id), encodedCols], { timeoutMs: 120_000 })
|
|
160
|
+
.catch(() => { });
|
|
161
|
+
}
|
|
162
|
+
// Refetch so attachments metadata is accurate, but redact secrets by default.
|
|
163
|
+
const { stdout: gotOut } = await this.bw.runForSession(session, ['get', 'item', String(created.id)], { timeoutMs: 60_000 });
|
|
164
|
+
const got = JSON.parse(gotOut);
|
|
165
|
+
return this.maybeRedact(got, input.reveal);
|
|
166
|
+
}
|
|
167
|
+
syncOnWrite() {
|
|
168
|
+
return ((process.env.KEYCHAIN_SYNC_ON_WRITE ?? 'true').toLowerCase() === 'true');
|
|
169
|
+
}
|
|
170
|
+
maybeRedact(value, reveal) {
|
|
171
|
+
return (reveal ? value : redactItem(value));
|
|
172
|
+
}
|
|
173
|
+
valueResult(value, revealed) {
|
|
174
|
+
return { value, revealed };
|
|
175
|
+
}
|
|
176
|
+
tryParseJson(stdout) {
|
|
177
|
+
const trimmed = stdout.trim();
|
|
178
|
+
if (!trimmed)
|
|
179
|
+
return '';
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(trimmed);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return trimmed;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async readSingleFileAsBase64(dir) {
|
|
188
|
+
const files = await readdir(dir);
|
|
189
|
+
if (files.length !== 1) {
|
|
190
|
+
throw new Error(`Expected exactly 1 downloaded file, found ${files.length}`);
|
|
191
|
+
}
|
|
192
|
+
const filename = files[0] ?? '';
|
|
193
|
+
const buf = await readFile(join(dir, filename));
|
|
194
|
+
return {
|
|
195
|
+
filename,
|
|
196
|
+
bytes: buf.byteLength,
|
|
197
|
+
contentBase64: buf.toString('base64'),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
redactPasswordHistoryForTool(history) {
|
|
201
|
+
// For secret-returning tools we avoid returning sentinel strings like "[REDACTED]"
|
|
202
|
+
// because downstream utilities might accidentally pass them through.
|
|
203
|
+
return history.map((h) => {
|
|
204
|
+
if (!h || typeof h !== 'object')
|
|
205
|
+
return h;
|
|
206
|
+
const rec = { ...h };
|
|
207
|
+
if (typeof rec.password === 'string')
|
|
208
|
+
rec.password = null;
|
|
209
|
+
return rec;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async status() {
|
|
213
|
+
return this.bw.status();
|
|
214
|
+
}
|
|
215
|
+
async encode(input) {
|
|
216
|
+
// `bw encode` base64-encodes stdin.
|
|
217
|
+
const { stdout } = await this.bw.withSession(async (session) => {
|
|
218
|
+
return this.bw.runForSession(session, ['encode'], {
|
|
219
|
+
stdin: `${input.value}\n`,
|
|
220
|
+
timeoutMs: 30_000,
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
return { encoded: stdout.trim() };
|
|
224
|
+
}
|
|
225
|
+
async generate(input = {}) {
|
|
226
|
+
if (!input.reveal)
|
|
227
|
+
return this.valueResult(null, false);
|
|
228
|
+
const args = buildBwGenerateArgs(input);
|
|
229
|
+
const { stdout } = await this.bw.withSession(async (session) => this.bw.runForSession(session, args, { timeoutMs: 30_000 }));
|
|
230
|
+
return this.valueResult(stdout.trim(), true);
|
|
231
|
+
}
|
|
232
|
+
async generateUsername(input = {}) {
|
|
233
|
+
if (!input.reveal)
|
|
234
|
+
return this.valueResult(null, false);
|
|
235
|
+
const value = generateUsername(input);
|
|
236
|
+
return this.valueResult(value, true);
|
|
237
|
+
}
|
|
238
|
+
async getAttachment(input) {
|
|
239
|
+
return this.bw.withSession(async (session) => {
|
|
240
|
+
const dir = await mkdtemp(join(tmpdir(), 'keychain-attachment-'));
|
|
241
|
+
try {
|
|
242
|
+
await this.bw.runForSession(session, [
|
|
243
|
+
'get',
|
|
244
|
+
'attachment',
|
|
245
|
+
input.attachmentId,
|
|
246
|
+
'--itemid',
|
|
247
|
+
input.itemId,
|
|
248
|
+
'--output',
|
|
249
|
+
dir,
|
|
250
|
+
], { timeoutMs: 120_000 });
|
|
251
|
+
return await this.readSingleFileAsBase64(dir);
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
await rm(dir, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async sendList() {
|
|
259
|
+
return this.bw.withSession(async (session) => {
|
|
260
|
+
const { stdout } = await this.bw.runForSession(session, ['send', 'list'], {
|
|
261
|
+
timeoutMs: 60_000,
|
|
262
|
+
});
|
|
263
|
+
return this.tryParseJson(stdout);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
async sendTemplate(input) {
|
|
267
|
+
return this.bw.withSession(async (session) => {
|
|
268
|
+
const { stdout } = await this.bw.runForSession(session, ['send', 'template', input.object], { timeoutMs: 60_000 });
|
|
269
|
+
return this.tryParseJson(stdout);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async sendGet(input) {
|
|
273
|
+
return this.bw.withSession(async (session) => {
|
|
274
|
+
if (input.text) {
|
|
275
|
+
const { stdout } = await this.bw.runForSession(session, ['--raw', 'send', 'get', input.id, '--text'], { timeoutMs: 60_000 });
|
|
276
|
+
return { text: stdout.trim() };
|
|
277
|
+
}
|
|
278
|
+
if (input.downloadFile) {
|
|
279
|
+
const dir = await mkdtemp(join(tmpdir(), 'keychain-sendfile-'));
|
|
280
|
+
try {
|
|
281
|
+
await this.bw.runForSession(session, ['send', 'get', input.id, '--output', dir], { timeoutMs: 120_000 });
|
|
282
|
+
const file = await this.readSingleFileAsBase64(dir);
|
|
283
|
+
return { file };
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
await rm(dir, { recursive: true, force: true });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const { stdout } = await this.bw.runForSession(session, ['send', 'get', input.id], {
|
|
290
|
+
timeoutMs: 60_000,
|
|
291
|
+
});
|
|
292
|
+
return this.tryParseJson(stdout);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async sendRemovePassword(input) {
|
|
296
|
+
return this.bw.withSession(async (session) => {
|
|
297
|
+
const { stdout } = await this.bw.runForSession(session, ['send', 'remove-password', input.id], { timeoutMs: 60_000 });
|
|
298
|
+
return this.tryParseJson(stdout);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
async sendDelete(input) {
|
|
302
|
+
return this.bw.withSession(async (session) => {
|
|
303
|
+
const { stdout } = await this.bw.runForSession(session, ['send', 'delete', input.id], {
|
|
304
|
+
timeoutMs: 60_000,
|
|
305
|
+
});
|
|
306
|
+
return this.tryParseJson(stdout);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async sendCreate(input) {
|
|
310
|
+
return this.bw.withSession(async (session) => {
|
|
311
|
+
const args = ['send'];
|
|
312
|
+
if (input.type === 'file')
|
|
313
|
+
args.push('--file');
|
|
314
|
+
if (typeof input.deleteInDays === 'number')
|
|
315
|
+
args.push('--deleteInDays', String(input.deleteInDays));
|
|
316
|
+
if (typeof input.password === 'string')
|
|
317
|
+
args.push('--password', input.password);
|
|
318
|
+
if (typeof input.maxAccessCount === 'number')
|
|
319
|
+
args.push('--maxAccessCount', String(input.maxAccessCount));
|
|
320
|
+
if (input.hidden)
|
|
321
|
+
args.push('--hidden');
|
|
322
|
+
if (typeof input.name === 'string')
|
|
323
|
+
args.push('--name', input.name);
|
|
324
|
+
if (typeof input.notes === 'string')
|
|
325
|
+
args.push('--notes', input.notes);
|
|
326
|
+
if (input.fullObject)
|
|
327
|
+
args.push('--fullObject');
|
|
328
|
+
if (input.type === 'text') {
|
|
329
|
+
if (typeof input.text !== 'string')
|
|
330
|
+
throw new Error('Missing text for text send');
|
|
331
|
+
const { stdout } = await this.bw.runForSession(session, ['--raw', ...args, input.text], { timeoutMs: 60_000 });
|
|
332
|
+
return this.tryParseJson(stdout);
|
|
333
|
+
}
|
|
334
|
+
if (typeof input.filename !== 'string' ||
|
|
335
|
+
typeof input.contentBase64 !== 'string') {
|
|
336
|
+
throw new Error('Missing filename/contentBase64 for file send');
|
|
337
|
+
}
|
|
338
|
+
const dir = await mkdtemp(join(tmpdir(), 'keychain-send-create-'));
|
|
339
|
+
const filePath = join(dir, basename(input.filename));
|
|
340
|
+
try {
|
|
341
|
+
await writeFile(filePath, Buffer.from(input.contentBase64, 'base64'));
|
|
342
|
+
const { stdout } = await this.bw.runForSession(session, ['--raw', ...args, filePath], { timeoutMs: 120_000 });
|
|
343
|
+
return this.tryParseJson(stdout);
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
await rm(dir, { recursive: true, force: true });
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
async sendCreateEncoded(input) {
|
|
351
|
+
return this.bw.withSession(async (session) => {
|
|
352
|
+
let encodedJson = input.encodedJson;
|
|
353
|
+
if (!encodedJson && typeof input.json !== 'undefined') {
|
|
354
|
+
const enc = await this.encode({ value: JSON.stringify(input.json) });
|
|
355
|
+
encodedJson = enc.encoded;
|
|
356
|
+
}
|
|
357
|
+
if (typeof encodedJson !== 'string' &&
|
|
358
|
+
typeof input.text !== 'string' &&
|
|
359
|
+
typeof input.file === 'undefined') {
|
|
360
|
+
throw new Error('sendCreateEncoded requires one of: encodedJson, json, text, or file');
|
|
361
|
+
}
|
|
362
|
+
const args = ['send', 'create'];
|
|
363
|
+
if (typeof input.text === 'string')
|
|
364
|
+
args.push('--text', input.text);
|
|
365
|
+
if (input.hidden)
|
|
366
|
+
args.push('--hidden');
|
|
367
|
+
let dir = null;
|
|
368
|
+
try {
|
|
369
|
+
if (input.file) {
|
|
370
|
+
dir = await mkdtemp(join(tmpdir(), 'keychain-send-create-'));
|
|
371
|
+
const filePath = join(dir, basename(input.file.filename));
|
|
372
|
+
await writeFile(filePath, Buffer.from(input.file.contentBase64, 'base64'));
|
|
373
|
+
args.push('--file', filePath);
|
|
374
|
+
}
|
|
375
|
+
if (typeof encodedJson === 'string')
|
|
376
|
+
args.push(encodedJson);
|
|
377
|
+
const { stdout } = await this.bw.runForSession(session, args, {
|
|
378
|
+
timeoutMs: 120_000,
|
|
379
|
+
});
|
|
380
|
+
return this.tryParseJson(stdout);
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
if (dir)
|
|
384
|
+
await rm(dir, { recursive: true, force: true });
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
async sendEdit(input) {
|
|
389
|
+
return this.bw.withSession(async (session) => {
|
|
390
|
+
let encodedJson = input.encodedJson;
|
|
391
|
+
if (!encodedJson && typeof input.json !== 'undefined') {
|
|
392
|
+
const enc = await this.encode({ value: JSON.stringify(input.json) });
|
|
393
|
+
encodedJson = enc.encoded;
|
|
394
|
+
}
|
|
395
|
+
if (typeof encodedJson !== 'string') {
|
|
396
|
+
throw new Error('sendEdit requires encodedJson or json');
|
|
397
|
+
}
|
|
398
|
+
const args = ['send', 'edit'];
|
|
399
|
+
if (typeof input.itemId === 'string')
|
|
400
|
+
args.push('--itemid', input.itemId);
|
|
401
|
+
args.push(encodedJson);
|
|
402
|
+
const { stdout } = await this.bw.runForSession(session, args, {
|
|
403
|
+
timeoutMs: 120_000,
|
|
404
|
+
});
|
|
405
|
+
return this.tryParseJson(stdout);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
async receive(input) {
|
|
409
|
+
return this.bw.withSession(async (session) => {
|
|
410
|
+
const args = ['receive', input.url];
|
|
411
|
+
if (typeof input.password === 'string')
|
|
412
|
+
args.push('--password', input.password);
|
|
413
|
+
if (input.obj) {
|
|
414
|
+
const { stdout } = await this.bw.runForSession(session, ['--raw', ...args, '--obj'], { timeoutMs: 60_000 });
|
|
415
|
+
return this.tryParseJson(stdout);
|
|
416
|
+
}
|
|
417
|
+
if (input.downloadFile) {
|
|
418
|
+
const dir = await mkdtemp(join(tmpdir(), 'keychain-receive-'));
|
|
419
|
+
const outPath = join(dir, 'received');
|
|
420
|
+
try {
|
|
421
|
+
await this.bw.runForSession(session, [...args, '--output', outPath], {
|
|
422
|
+
timeoutMs: 120_000,
|
|
423
|
+
});
|
|
424
|
+
const buf = await readFile(outPath);
|
|
425
|
+
return {
|
|
426
|
+
file: {
|
|
427
|
+
filename: basename(outPath),
|
|
428
|
+
bytes: buf.byteLength,
|
|
429
|
+
contentBase64: buf.toString('base64'),
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
finally {
|
|
434
|
+
await rm(dir, { recursive: true, force: true });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const { stdout } = await this.bw.runForSession(session, ['--raw', ...args], {
|
|
438
|
+
timeoutMs: 60_000,
|
|
439
|
+
});
|
|
440
|
+
return { text: stdout.trim() };
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
async searchItems(input) {
|
|
444
|
+
const { limit } = input;
|
|
445
|
+
const rawText = (input.text ?? '').trim();
|
|
446
|
+
const tokens = rawText.includes('|')
|
|
447
|
+
? rawText
|
|
448
|
+
.split('|')
|
|
449
|
+
.map((s) => s.trim())
|
|
450
|
+
.filter((s) => s.length > 0)
|
|
451
|
+
: rawText.length > 0
|
|
452
|
+
? [rawText]
|
|
453
|
+
: [];
|
|
454
|
+
const orgFilter = input.organizationId;
|
|
455
|
+
const orgId = orgFilter && orgFilter !== 'null' && orgFilter !== 'notnull'
|
|
456
|
+
? orgFilter
|
|
457
|
+
: undefined;
|
|
458
|
+
const folderFilter = input.folderId;
|
|
459
|
+
const folderId = folderFilter && folderFilter !== 'null' && folderFilter !== 'notnull'
|
|
460
|
+
? folderFilter
|
|
461
|
+
: undefined;
|
|
462
|
+
const items = await this.bw.withSession(async (session) => {
|
|
463
|
+
const baseArgs = ['list', 'items'];
|
|
464
|
+
if (input.url)
|
|
465
|
+
baseArgs.push('--url', input.url);
|
|
466
|
+
if (folderId)
|
|
467
|
+
baseArgs.push('--folderid', folderId);
|
|
468
|
+
if (input.collectionId)
|
|
469
|
+
baseArgs.push('--collectionid', input.collectionId);
|
|
470
|
+
if (orgId)
|
|
471
|
+
baseArgs.push('--organizationid', orgId);
|
|
472
|
+
if (input.trash)
|
|
473
|
+
baseArgs.push('--trash');
|
|
474
|
+
// NOTE: bw's `--search` does not treat "a | b" as "a OR b". If callers pass
|
|
475
|
+
// a pipe-delimited string (common when combining name + username), we split
|
|
476
|
+
// and union the results.
|
|
477
|
+
const terms = tokens.length ? tokens : [undefined];
|
|
478
|
+
const byId = new Map();
|
|
479
|
+
for (const term of terms) {
|
|
480
|
+
const args = [...baseArgs];
|
|
481
|
+
if (term)
|
|
482
|
+
args.push('--search', term);
|
|
483
|
+
const { stdout } = await this.bw.runForSession(session, args, {
|
|
484
|
+
timeoutMs: 120_000,
|
|
485
|
+
});
|
|
486
|
+
const results = JSON.parse(stdout);
|
|
487
|
+
for (const raw of results) {
|
|
488
|
+
if (!raw || typeof raw !== 'object')
|
|
489
|
+
continue;
|
|
490
|
+
const id = raw.id;
|
|
491
|
+
if (typeof id === 'string' && id.length > 0)
|
|
492
|
+
byId.set(id, raw);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return [...byId.values()];
|
|
496
|
+
});
|
|
497
|
+
const orgFiltered = items.filter((raw) => {
|
|
498
|
+
if (!raw || typeof raw !== 'object')
|
|
499
|
+
return false;
|
|
500
|
+
const item = raw;
|
|
501
|
+
if (orgFilter === 'null') {
|
|
502
|
+
return item.organizationId == null;
|
|
503
|
+
}
|
|
504
|
+
if (orgFilter === 'notnull') {
|
|
505
|
+
return typeof item.organizationId === 'string' && item.organizationId;
|
|
506
|
+
}
|
|
507
|
+
return true;
|
|
508
|
+
});
|
|
509
|
+
const folderFiltered = orgFiltered.filter((raw) => {
|
|
510
|
+
if (!raw || typeof raw !== 'object')
|
|
511
|
+
return false;
|
|
512
|
+
const item = raw;
|
|
513
|
+
if (folderFilter === 'null') {
|
|
514
|
+
return item.folderId == null;
|
|
515
|
+
}
|
|
516
|
+
if (folderFilter === 'notnull') {
|
|
517
|
+
return typeof item.folderId === 'string' && item.folderId;
|
|
518
|
+
}
|
|
519
|
+
return true;
|
|
520
|
+
});
|
|
521
|
+
const filtered = folderFiltered.filter((raw) => {
|
|
522
|
+
if (!raw || typeof raw !== 'object')
|
|
523
|
+
return false;
|
|
524
|
+
const item = raw;
|
|
525
|
+
if (!input.type)
|
|
526
|
+
return true;
|
|
527
|
+
if (input.type === 'ssh_key')
|
|
528
|
+
return isSshKeyItem(item);
|
|
529
|
+
if (input.type === 'login')
|
|
530
|
+
return item.type === ITEM_TYPE.login;
|
|
531
|
+
if (input.type === 'card')
|
|
532
|
+
return item.type === ITEM_TYPE.card;
|
|
533
|
+
if (input.type === 'identity')
|
|
534
|
+
return item.type === ITEM_TYPE.identity;
|
|
535
|
+
if (input.type === 'note')
|
|
536
|
+
return item.type === ITEM_TYPE.note && !isSshKeyItem(item);
|
|
537
|
+
return true;
|
|
538
|
+
});
|
|
539
|
+
return typeof limit === 'number' ? filtered.slice(0, limit) : filtered;
|
|
540
|
+
}
|
|
541
|
+
async listFolders(input = {}) {
|
|
542
|
+
const { limit } = input;
|
|
543
|
+
const folders = await this.bw.withSession(async (session) => {
|
|
544
|
+
const args = ['list', 'folders'];
|
|
545
|
+
if (input.search)
|
|
546
|
+
args.push('--search', input.search);
|
|
547
|
+
const { stdout } = await this.bw.runForSession(session, args, {
|
|
548
|
+
timeoutMs: 60_000,
|
|
549
|
+
});
|
|
550
|
+
return JSON.parse(stdout);
|
|
551
|
+
});
|
|
552
|
+
return typeof limit === 'number' ? folders.slice(0, limit) : folders;
|
|
553
|
+
}
|
|
554
|
+
async listCollections(input = {}) {
|
|
555
|
+
const { limit } = input;
|
|
556
|
+
const collections = await this.bw.withSession(async (session) => {
|
|
557
|
+
const args = ['list', 'collections'];
|
|
558
|
+
if (input.search)
|
|
559
|
+
args.push('--search', input.search);
|
|
560
|
+
if (input.organizationId)
|
|
561
|
+
args.push('--organizationid', input.organizationId);
|
|
562
|
+
const { stdout } = await this.bw.runForSession(session, args, {
|
|
563
|
+
timeoutMs: 60_000,
|
|
564
|
+
});
|
|
565
|
+
return JSON.parse(stdout);
|
|
566
|
+
});
|
|
567
|
+
return typeof limit === 'number'
|
|
568
|
+
? collections.slice(0, limit)
|
|
569
|
+
: collections;
|
|
570
|
+
}
|
|
571
|
+
async listOrganizations(input = {}) {
|
|
572
|
+
const { limit } = input;
|
|
573
|
+
const orgs = await this.bw.withSession(async (session) => {
|
|
574
|
+
const args = ['list', 'organizations'];
|
|
575
|
+
if (input.search)
|
|
576
|
+
args.push('--search', input.search);
|
|
577
|
+
const { stdout } = await this.bw.runForSession(session, args, {
|
|
578
|
+
timeoutMs: 60_000,
|
|
579
|
+
});
|
|
580
|
+
return JSON.parse(stdout);
|
|
581
|
+
});
|
|
582
|
+
return typeof limit === 'number' ? orgs.slice(0, limit) : orgs;
|
|
583
|
+
}
|
|
584
|
+
async createFolder(name) {
|
|
585
|
+
return this.bw.withSession(async (session) => {
|
|
586
|
+
if (this.syncOnWrite()) {
|
|
587
|
+
await this.bw
|
|
588
|
+
.runForSession(session, ['sync'], { timeoutMs: 120_000 })
|
|
589
|
+
.catch(() => { });
|
|
590
|
+
}
|
|
591
|
+
const encoded = encodeJsonForBw({ name });
|
|
592
|
+
const { stdout } = await this.bw.runForSession(session, ['create', 'folder', encoded], { timeoutMs: 60_000 });
|
|
593
|
+
return JSON.parse(stdout);
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
async editFolder(input) {
|
|
597
|
+
return this.bw.withSession(async (session) => {
|
|
598
|
+
if (this.syncOnWrite()) {
|
|
599
|
+
await this.bw
|
|
600
|
+
.runForSession(session, ['sync'], { timeoutMs: 120_000 })
|
|
601
|
+
.catch(() => { });
|
|
602
|
+
}
|
|
603
|
+
const encoded = encodeJsonForBw({ name: input.name });
|
|
604
|
+
const { stdout } = await this.bw.runForSession(session, ['edit', 'folder', input.id, encoded], { timeoutMs: 60_000 });
|
|
605
|
+
return JSON.parse(stdout);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
async deleteFolder(input) {
|
|
609
|
+
return this.bw.withSession(async (session) => {
|
|
610
|
+
if (this.syncOnWrite()) {
|
|
611
|
+
await this.bw
|
|
612
|
+
.runForSession(session, ['sync'], { timeoutMs: 120_000 })
|
|
613
|
+
.catch(() => { });
|
|
614
|
+
}
|
|
615
|
+
await this.bw.runForSession(session, ['delete', 'folder', input.id], {
|
|
616
|
+
timeoutMs: 60_000,
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
async listOrgCollections(input) {
|
|
621
|
+
const { limit } = input;
|
|
622
|
+
const cols = await this.bw.withSession(async (session) => {
|
|
623
|
+
const args = [
|
|
624
|
+
'list',
|
|
625
|
+
'org-collections',
|
|
626
|
+
'--organizationid',
|
|
627
|
+
input.organizationId,
|
|
628
|
+
];
|
|
629
|
+
if (input.search)
|
|
630
|
+
args.push('--search', input.search);
|
|
631
|
+
const { stdout } = await this.bw.runForSession(session, args, {
|
|
632
|
+
timeoutMs: 60_000,
|
|
633
|
+
});
|
|
634
|
+
return JSON.parse(stdout);
|
|
635
|
+
});
|
|
636
|
+
return typeof limit === 'number' ? cols.slice(0, limit) : cols;
|
|
637
|
+
}
|
|
638
|
+
async createOrgCollection(input) {
|
|
639
|
+
return this.bw.withSession(async (session) => {
|
|
640
|
+
if (this.syncOnWrite()) {
|
|
641
|
+
await this.bw
|
|
642
|
+
.runForSession(session, ['sync'], { timeoutMs: 120_000 })
|
|
643
|
+
.catch(() => { });
|
|
644
|
+
}
|
|
645
|
+
// Newer bw CLI versions validate that --organizationid matches the request payload.
|
|
646
|
+
const encoded = encodeJsonForBw({
|
|
647
|
+
name: input.name,
|
|
648
|
+
organizationId: input.organizationId,
|
|
649
|
+
});
|
|
650
|
+
const { stdout } = await this.bw.runForSession(session, [
|
|
651
|
+
'create',
|
|
652
|
+
'org-collection',
|
|
653
|
+
'--organizationid',
|
|
654
|
+
input.organizationId,
|
|
655
|
+
encoded,
|
|
656
|
+
], { timeoutMs: 60_000 });
|
|
657
|
+
return JSON.parse(stdout);
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
async editOrgCollection(input) {
|
|
661
|
+
return this.bw.withSession(async (session) => {
|
|
662
|
+
if (this.syncOnWrite()) {
|
|
663
|
+
await this.bw
|
|
664
|
+
.runForSession(session, ['sync'], { timeoutMs: 120_000 })
|
|
665
|
+
.catch(() => { });
|
|
666
|
+
}
|
|
667
|
+
// Newer bw CLI versions validate that --organizationid matches the request payload.
|
|
668
|
+
const encoded = encodeJsonForBw({
|
|
669
|
+
id: input.id,
|
|
670
|
+
name: input.name,
|
|
671
|
+
organizationId: input.organizationId,
|
|
672
|
+
});
|
|
673
|
+
const { stdout } = await this.bw.runForSession(session, [
|
|
674
|
+
'edit',
|
|
675
|
+
'org-collection',
|
|
676
|
+
input.id,
|
|
677
|
+
encoded,
|
|
678
|
+
'--organizationid',
|
|
679
|
+
input.organizationId,
|
|
680
|
+
], { timeoutMs: 60_000 });
|
|
681
|
+
return JSON.parse(stdout);
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
async deleteOrgCollection(input) {
|
|
685
|
+
return this.bw.withSession(async (session) => {
|
|
686
|
+
if (this.syncOnWrite()) {
|
|
687
|
+
await this.bw
|
|
688
|
+
.runForSession(session, ['sync'], { timeoutMs: 120_000 })
|
|
689
|
+
.catch(() => { });
|
|
690
|
+
}
|
|
691
|
+
await this.bw.runForSession(session, [
|
|
692
|
+
'delete',
|
|
693
|
+
'org-collection',
|
|
694
|
+
input.id,
|
|
695
|
+
'--organizationid',
|
|
696
|
+
input.organizationId,
|
|
697
|
+
], { timeoutMs: 60_000 });
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
async moveItemToOrganization(input) {
|
|
701
|
+
return this.bw.withSession(async (session) => {
|
|
702
|
+
if (this.syncOnWrite()) {
|
|
703
|
+
await this.bw
|
|
704
|
+
.runForSession(session, ['sync'], { timeoutMs: 120_000 })
|
|
705
|
+
.catch(() => { });
|
|
706
|
+
}
|
|
707
|
+
const args = ['move', input.id, input.organizationId];
|
|
708
|
+
if (input.collectionIds) {
|
|
709
|
+
args.push(encodeJsonForBw(input.collectionIds));
|
|
710
|
+
}
|
|
711
|
+
const { stdout } = await this.bw.runForSession(session, args, {
|
|
712
|
+
timeoutMs: 120_000,
|
|
713
|
+
});
|
|
714
|
+
const moved = JSON.parse(stdout);
|
|
715
|
+
return this.maybeRedact(moved, input.reveal);
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
async getItem(id, opts = {}) {
|
|
719
|
+
const item = await this.bw.withSession(async (session) => {
|
|
720
|
+
const { stdout } = await this.bw.runForSession(session, ['get', 'item', id], { timeoutMs: 60_000 });
|
|
721
|
+
return JSON.parse(stdout);
|
|
722
|
+
});
|
|
723
|
+
return this.maybeRedact(item, opts.reveal);
|
|
724
|
+
}
|
|
725
|
+
async deleteItem(input) {
|
|
726
|
+
return this.bw.withSession(async (session) => {
|
|
727
|
+
if (this.syncOnWrite()) {
|
|
728
|
+
await this.bw
|
|
729
|
+
.runForSession(session, ['sync'], { timeoutMs: 120_000 })
|
|
730
|
+
.catch(() => { });
|
|
731
|
+
}
|
|
732
|
+
const args = ['delete', 'item', input.id];
|
|
733
|
+
if (input.permanent)
|
|
734
|
+
args.push('--permanent');
|
|
735
|
+
await this.bw.runForSession(session, args, { timeoutMs: 60_000 });
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
async deleteItems(input) {
|
|
739
|
+
if (input.ids.length === 0)
|
|
740
|
+
return [];
|
|
741
|
+
if (input.ids.length > 200)
|
|
742
|
+
throw new Error('Too many ids (max 200)');
|
|
743
|
+
// Run inside a single session lock to avoid re-syncing/unlocking per item.
|
|
744
|
+
return this.bw.withSession(async (session) => {
|
|
745
|
+
if (this.syncOnWrite()) {
|
|
746
|
+
await this.bw
|
|
747
|
+
.runForSession(session, ['sync'], { timeoutMs: 120_000 })
|
|
748
|
+
.catch(() => { });
|
|
749
|
+
}
|
|
750
|
+
const results = [];
|
|
751
|
+
for (const id of input.ids) {
|
|
752
|
+
try {
|
|
753
|
+
const args = ['delete', 'item', id];
|
|
754
|
+
if (input.permanent)
|
|
755
|
+
args.push('--permanent');
|
|
756
|
+
await this.bw.runForSession(session, args, { timeoutMs: 60_000 });
|
|
757
|
+
results.push({ id, ok: true });
|
|
758
|
+
}
|
|
759
|
+
catch (e) {
|
|
760
|
+
results.push({
|
|
761
|
+
id,
|
|
762
|
+
ok: false,
|
|
763
|
+
error: e instanceof Error ? e.message : String(e),
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return results;
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
async restoreItem(input) {
|
|
771
|
+
return this.bw.withSession(async (session) => {
|
|
772
|
+
if (this.syncOnWrite()) {
|
|
773
|
+
await this.bw
|
|
774
|
+
.runForSession(session, ['sync'], { timeoutMs: 120_000 })
|
|
775
|
+
.catch(() => { });
|
|
776
|
+
}
|
|
777
|
+
const { stdout } = await this.bw.runForSession(session, ['restore', 'item', input.id], { timeoutMs: 60_000 });
|
|
778
|
+
// restore may not return JSON; ignore stdout and refetch.
|
|
779
|
+
void stdout;
|
|
780
|
+
const { stdout: gotOut } = await this.bw.runForSession(session, ['get', 'item', input.id], { timeoutMs: 60_000 });
|
|
781
|
+
return this.maybeRedact(JSON.parse(gotOut), input.reveal);
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
async createAttachment(input) {
|
|
785
|
+
return this.bw.withSession(async (session) => {
|
|
786
|
+
if (this.syncOnWrite()) {
|
|
787
|
+
await this.bw
|
|
788
|
+
.runForSession(session, ['sync'], { timeoutMs: 120_000 })
|
|
789
|
+
.catch(() => { });
|
|
790
|
+
}
|
|
791
|
+
const dir = await mkdtemp(join(tmpdir(), 'keychain-attach-'));
|
|
792
|
+
try {
|
|
793
|
+
const safeBase = basename(input.filename || 'attachment.bin').replace(/[^A-Za-z0-9._-]+/g, '_');
|
|
794
|
+
const safeName = safeBase.length > 0 ? safeBase : 'attachment.bin';
|
|
795
|
+
const p = join(dir, safeName);
|
|
796
|
+
await writeFile(p, Buffer.from(input.contentBase64, 'base64'));
|
|
797
|
+
const { stdout: out } = await this.bw.runForSession(session, ['create', 'attachment', '--file', p, '--itemid', input.itemId], { timeoutMs: 120_000 });
|
|
798
|
+
// bw may return a full item JSON; ignore it and refetch the item.
|
|
799
|
+
void out;
|
|
800
|
+
}
|
|
801
|
+
finally {
|
|
802
|
+
await rm(dir, { recursive: true, force: true });
|
|
803
|
+
}
|
|
804
|
+
const { stdout: gotOut } = await this.bw.runForSession(session, ['get', 'item', input.itemId], { timeoutMs: 60_000 });
|
|
805
|
+
return this.maybeRedact(JSON.parse(gotOut), input.reveal);
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
async deleteAttachment(input) {
|
|
809
|
+
return this.bw.withSession(async (session) => {
|
|
810
|
+
if (this.syncOnWrite()) {
|
|
811
|
+
await this.bw
|
|
812
|
+
.runForSession(session, ['sync'], { timeoutMs: 120_000 })
|
|
813
|
+
.catch(() => { });
|
|
814
|
+
}
|
|
815
|
+
await this.bw.runForSession(session, ['delete', 'attachment', input.attachmentId, '--itemid', input.itemId], { timeoutMs: 60_000 });
|
|
816
|
+
const { stdout: gotOut } = await this.bw.runForSession(session, ['get', 'item', input.itemId], { timeoutMs: 60_000 });
|
|
817
|
+
return this.maybeRedact(JSON.parse(gotOut), input.reveal);
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
async getUsername(input) {
|
|
821
|
+
return this.bw.withSession(async (session) => {
|
|
822
|
+
const { stdout } = await this.bw.runForSession(session, ['--raw', 'get', 'username', input.term], { timeoutMs: 60_000 });
|
|
823
|
+
return this.valueResult(stdout.trim(), true);
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
async getPassword(input, opts = {}) {
|
|
827
|
+
if (!opts.reveal)
|
|
828
|
+
return this.valueResult(null, false);
|
|
829
|
+
return this.bw.withSession(async (session) => {
|
|
830
|
+
const { stdout } = await this.bw.runForSession(session, ['--raw', 'get', 'password', input.term], { timeoutMs: 60_000 });
|
|
831
|
+
return this.valueResult(stdout.trim(), true);
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
async getTotp(input, opts = {}) {
|
|
835
|
+
if (!opts.reveal)
|
|
836
|
+
return this.valueResult(null, false);
|
|
837
|
+
return this.bw.withSession(async (session) => {
|
|
838
|
+
const { stdout } = await this.bw.runForSession(session, ['--raw', 'get', 'totp', input.term], { timeoutMs: 60_000 });
|
|
839
|
+
return this.valueResult(stdout.trim(), true);
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
async getUri(input) {
|
|
843
|
+
return this.bw.withSession(async (session) => {
|
|
844
|
+
const { stdout } = await this.bw.runForSession(session, ['--raw', 'get', 'uri', input.term], { timeoutMs: 60_000 });
|
|
845
|
+
return this.valueResult(stdout.trim(), true);
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
async getNotes(input, opts = {}) {
|
|
849
|
+
if (!opts.reveal)
|
|
850
|
+
return this.valueResult(null, false);
|
|
851
|
+
return this.bw.withSession(async (session) => {
|
|
852
|
+
const { stdout } = await this.bw.runForSession(session, ['--raw', 'get', 'notes', input.term], { timeoutMs: 60_000 });
|
|
853
|
+
return this.valueResult(stdout.trim(), true);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
async getExposed(input) {
|
|
857
|
+
const isNotFoundError = (err) => {
|
|
858
|
+
const combined = `${err.stderr}\n${err.stdout}`.trim().toLowerCase();
|
|
859
|
+
if (/more than one result/.test(combined)) {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
if (/could not connect/.test(combined) ||
|
|
863
|
+
/connection/.test(combined) ||
|
|
864
|
+
/network/.test(combined) ||
|
|
865
|
+
/timeout/.test(combined) ||
|
|
866
|
+
/unauthorized|forbidden|not logged|authentication|permission/.test(combined)) {
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
return true;
|
|
870
|
+
};
|
|
871
|
+
return this.bw.withSession(async (session) => {
|
|
872
|
+
try {
|
|
873
|
+
const { stdout } = await this.bw.runForSession(session, ['--raw', 'get', 'exposed', input.term], { timeoutMs: 60_000 });
|
|
874
|
+
return this.valueResult(stdout.trim(), true);
|
|
875
|
+
}
|
|
876
|
+
catch (err) {
|
|
877
|
+
if ((err instanceof BwCliError &&
|
|
878
|
+
err.exitCode === 1 &&
|
|
879
|
+
isNotFoundError(err)) ||
|
|
880
|
+
(err instanceof Error &&
|
|
881
|
+
/exit code 1/.test(err.message.toLowerCase()))) {
|
|
882
|
+
return this.valueResult(null, false);
|
|
883
|
+
}
|
|
884
|
+
throw err;
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
async getFolder(input) {
|
|
889
|
+
return this.bw.withSession(async (session) => {
|
|
890
|
+
const { stdout } = await this.bw.runForSession(session, ['get', 'folder', input.id], { timeoutMs: 60_000 });
|
|
891
|
+
return JSON.parse(stdout);
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
async getCollection(input) {
|
|
895
|
+
return this.bw.withSession(async (session) => {
|
|
896
|
+
const args = ['get', 'collection', input.id];
|
|
897
|
+
if (input.organizationId)
|
|
898
|
+
args.push('--organizationid', input.organizationId);
|
|
899
|
+
const { stdout } = await this.bw.runForSession(session, args, {
|
|
900
|
+
timeoutMs: 60_000,
|
|
901
|
+
});
|
|
902
|
+
return JSON.parse(stdout);
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
async getOrganization(input) {
|
|
906
|
+
return this.bw.withSession(async (session) => {
|
|
907
|
+
const { stdout } = await this.bw.runForSession(session, ['get', 'organization', input.id], { timeoutMs: 60_000 });
|
|
908
|
+
return JSON.parse(stdout);
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
async getOrgCollection(input) {
|
|
912
|
+
return this.bw.withSession(async (session) => {
|
|
913
|
+
const args = ['get', 'org-collection', input.id];
|
|
914
|
+
if (input.organizationId)
|
|
915
|
+
args.push('--organizationid', input.organizationId);
|
|
916
|
+
const { stdout } = await this.bw.runForSession(session, args, {
|
|
917
|
+
timeoutMs: 60_000,
|
|
918
|
+
});
|
|
919
|
+
return JSON.parse(stdout);
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
async getPasswordHistory(id, opts = {}) {
|
|
923
|
+
const item = await this.bw.withSession(async (session) => {
|
|
924
|
+
const { stdout } = await this.bw.runForSession(session, ['get', 'item', id], { timeoutMs: 60_000 });
|
|
925
|
+
return JSON.parse(stdout);
|
|
926
|
+
});
|
|
927
|
+
const history = Array.isArray(item.passwordHistory)
|
|
928
|
+
? item.passwordHistory
|
|
929
|
+
: [];
|
|
930
|
+
if (opts.reveal)
|
|
931
|
+
return { value: history, revealed: true };
|
|
932
|
+
return {
|
|
933
|
+
value: this.redactPasswordHistoryForTool(history),
|
|
934
|
+
revealed: false,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
async createLogin(input) {
|
|
938
|
+
return this.bw.withSession(async (session) => {
|
|
939
|
+
if (this.syncOnWrite()) {
|
|
940
|
+
await this.bw
|
|
941
|
+
.runForSession(session, ['sync'], {
|
|
942
|
+
timeoutMs: 120_000,
|
|
943
|
+
})
|
|
944
|
+
.catch(() => { });
|
|
945
|
+
}
|
|
946
|
+
return this.createLoginForSession(session, input);
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
async createLogins(input) {
|
|
950
|
+
const continueOnError = input.continueOnError ?? true;
|
|
951
|
+
return this.bw.withSession(async (session) => {
|
|
952
|
+
if (this.syncOnWrite()) {
|
|
953
|
+
await this.bw
|
|
954
|
+
.runForSession(session, ['sync'], { timeoutMs: 120_000 })
|
|
955
|
+
.catch(() => { });
|
|
956
|
+
}
|
|
957
|
+
const results = [];
|
|
958
|
+
for (const it of input.items) {
|
|
959
|
+
try {
|
|
960
|
+
const created = await this.createLoginForSession(session, it);
|
|
961
|
+
results.push({ ok: true, item: created });
|
|
962
|
+
}
|
|
963
|
+
catch (err) {
|
|
964
|
+
const msg = err && typeof err === 'object' && 'message' in err
|
|
965
|
+
? String(err.message)
|
|
966
|
+
: String(err);
|
|
967
|
+
results.push({ ok: false, error: msg });
|
|
968
|
+
if (!continueOnError)
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return results;
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
async createNote(input) {
|
|
976
|
+
return this.bw.withSession(async (session) => {
|
|
977
|
+
if (this.syncOnWrite()) {
|
|
978
|
+
await this.bw
|
|
979
|
+
.runForSession(session, ['sync'], {
|
|
980
|
+
timeoutMs: 120_000,
|
|
981
|
+
})
|
|
982
|
+
.catch(() => { });
|
|
983
|
+
}
|
|
984
|
+
const tpl = (await this.bw.getTemplateItemForSession(session));
|
|
985
|
+
const item = deepClone(tpl);
|
|
986
|
+
item.type = ITEM_TYPE.note;
|
|
987
|
+
item.name = input.name;
|
|
988
|
+
item.notes = input.notes ?? '';
|
|
989
|
+
item.favorite = input.favorite ?? false;
|
|
990
|
+
if (input.organizationId)
|
|
991
|
+
item.organizationId = input.organizationId;
|
|
992
|
+
if (input.folderId)
|
|
993
|
+
item.folderId = input.folderId;
|
|
994
|
+
if (!item.secureNote || typeof item.secureNote !== 'object') {
|
|
995
|
+
item.secureNote = { type: 0 };
|
|
996
|
+
}
|
|
997
|
+
item.fields = normalizeFields(input.fields) ?? [];
|
|
998
|
+
if (input.collectionIds)
|
|
999
|
+
item.collectionIds = input.collectionIds;
|
|
1000
|
+
const encoded = encodeJsonForBw(item);
|
|
1001
|
+
const { stdout } = await this.bw.runForSession(session, ['create', 'item', encoded], { timeoutMs: 120_000 });
|
|
1002
|
+
const created = JSON.parse(stdout);
|
|
1003
|
+
if (input.collectionIds?.length) {
|
|
1004
|
+
const encodedCols = encodeJsonForBw(input.collectionIds);
|
|
1005
|
+
await this.bw
|
|
1006
|
+
.runForSession(session, ['edit', 'item-collections', String(created.id), encodedCols], { timeoutMs: 120_000 })
|
|
1007
|
+
.catch(() => { });
|
|
1008
|
+
}
|
|
1009
|
+
return this.maybeRedact(created, input.reveal);
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
async createSshKey(input) {
|
|
1013
|
+
const fields = [
|
|
1014
|
+
{ name: 'public_key', value: input.publicKey, hidden: false },
|
|
1015
|
+
{ name: 'private_key', value: input.privateKey, hidden: true },
|
|
1016
|
+
];
|
|
1017
|
+
if (input.fingerprint)
|
|
1018
|
+
fields.push({
|
|
1019
|
+
name: 'fingerprint',
|
|
1020
|
+
value: input.fingerprint,
|
|
1021
|
+
hidden: false,
|
|
1022
|
+
});
|
|
1023
|
+
if (input.comment)
|
|
1024
|
+
fields.push({ name: 'comment', value: input.comment, hidden: false });
|
|
1025
|
+
return this.createNote({
|
|
1026
|
+
name: input.name,
|
|
1027
|
+
notes: input.notes,
|
|
1028
|
+
reveal: input.reveal,
|
|
1029
|
+
favorite: input.favorite,
|
|
1030
|
+
organizationId: input.organizationId,
|
|
1031
|
+
collectionIds: input.collectionIds,
|
|
1032
|
+
folderId: input.folderId,
|
|
1033
|
+
fields,
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
async createCard(input) {
|
|
1037
|
+
return this.bw.withSession(async (session) => {
|
|
1038
|
+
if (this.syncOnWrite()) {
|
|
1039
|
+
await this.bw
|
|
1040
|
+
.runForSession(session, ['sync'], { timeoutMs: 120_000 })
|
|
1041
|
+
.catch(() => { });
|
|
1042
|
+
}
|
|
1043
|
+
const tpl = (await this.bw.getTemplateItemForSession(session));
|
|
1044
|
+
const item = deepClone(tpl);
|
|
1045
|
+
item.type = ITEM_TYPE.card;
|
|
1046
|
+
item.name = input.name;
|
|
1047
|
+
item.notes = input.notes ?? '';
|
|
1048
|
+
item.favorite = input.favorite ?? false;
|
|
1049
|
+
if (input.organizationId)
|
|
1050
|
+
item.organizationId = input.organizationId;
|
|
1051
|
+
if (input.folderId)
|
|
1052
|
+
item.folderId = input.folderId;
|
|
1053
|
+
item.fields = normalizeFields(input.fields) ?? [];
|
|
1054
|
+
const card = (item.card && typeof item.card === 'object'
|
|
1055
|
+
? item.card
|
|
1056
|
+
: {});
|
|
1057
|
+
if (input.cardholderName !== undefined)
|
|
1058
|
+
card.cardholderName = input.cardholderName;
|
|
1059
|
+
if (input.brand !== undefined)
|
|
1060
|
+
card.brand = input.brand;
|
|
1061
|
+
if (input.number !== undefined)
|
|
1062
|
+
card.number = input.number;
|
|
1063
|
+
if (input.expMonth !== undefined)
|
|
1064
|
+
card.expMonth = input.expMonth;
|
|
1065
|
+
if (input.expYear !== undefined)
|
|
1066
|
+
card.expYear = input.expYear;
|
|
1067
|
+
if (input.code !== undefined)
|
|
1068
|
+
card.code = input.code;
|
|
1069
|
+
item.card = card;
|
|
1070
|
+
if (input.collectionIds)
|
|
1071
|
+
item.collectionIds = input.collectionIds;
|
|
1072
|
+
const encoded = encodeJsonForBw(item);
|
|
1073
|
+
const { stdout } = await this.bw.runForSession(session, ['create', 'item', encoded], { timeoutMs: 120_000 });
|
|
1074
|
+
const created = JSON.parse(stdout);
|
|
1075
|
+
if (input.collectionIds?.length) {
|
|
1076
|
+
const encodedCols = encodeJsonForBw(input.collectionIds);
|
|
1077
|
+
await this.bw
|
|
1078
|
+
.runForSession(session, ['edit', 'item-collections', String(created.id), encodedCols], { timeoutMs: 120_000 })
|
|
1079
|
+
.catch(() => { });
|
|
1080
|
+
}
|
|
1081
|
+
return this.maybeRedact(created, input.reveal);
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
async createIdentity(input) {
|
|
1085
|
+
return this.bw.withSession(async (session) => {
|
|
1086
|
+
if (this.syncOnWrite()) {
|
|
1087
|
+
await this.bw
|
|
1088
|
+
.runForSession(session, ['sync'], { timeoutMs: 120_000 })
|
|
1089
|
+
.catch(() => { });
|
|
1090
|
+
}
|
|
1091
|
+
const tpl = (await this.bw.getTemplateItemForSession(session));
|
|
1092
|
+
const item = deepClone(tpl);
|
|
1093
|
+
item.type = ITEM_TYPE.identity;
|
|
1094
|
+
item.name = input.name;
|
|
1095
|
+
item.notes = input.notes ?? '';
|
|
1096
|
+
item.favorite = input.favorite ?? false;
|
|
1097
|
+
if (input.organizationId)
|
|
1098
|
+
item.organizationId = input.organizationId;
|
|
1099
|
+
if (input.folderId)
|
|
1100
|
+
item.folderId = input.folderId;
|
|
1101
|
+
item.fields = normalizeFields(input.fields) ?? [];
|
|
1102
|
+
const identity = (item.identity && typeof item.identity === 'object'
|
|
1103
|
+
? item.identity
|
|
1104
|
+
: {});
|
|
1105
|
+
if (input.identity) {
|
|
1106
|
+
for (const [k, v] of Object.entries(input.identity)) {
|
|
1107
|
+
if (v !== undefined)
|
|
1108
|
+
identity[k] = v;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
item.identity = identity;
|
|
1112
|
+
if (input.collectionIds)
|
|
1113
|
+
item.collectionIds = input.collectionIds;
|
|
1114
|
+
const encoded = encodeJsonForBw(item);
|
|
1115
|
+
const { stdout } = await this.bw.runForSession(session, ['create', 'item', encoded], { timeoutMs: 120_000 });
|
|
1116
|
+
const created = JSON.parse(stdout);
|
|
1117
|
+
if (input.collectionIds?.length) {
|
|
1118
|
+
const encodedCols = encodeJsonForBw(input.collectionIds);
|
|
1119
|
+
await this.bw
|
|
1120
|
+
.runForSession(session, ['edit', 'item-collections', String(created.id), encodedCols], { timeoutMs: 120_000 })
|
|
1121
|
+
.catch(() => { });
|
|
1122
|
+
}
|
|
1123
|
+
return this.maybeRedact(created, input.reveal);
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
async updateItem(id, patch, opts = {}) {
|
|
1127
|
+
return this.bw.withSession(async (session) => {
|
|
1128
|
+
if (this.syncOnWrite()) {
|
|
1129
|
+
await this.bw
|
|
1130
|
+
.runForSession(session, ['sync'], {
|
|
1131
|
+
timeoutMs: 120_000,
|
|
1132
|
+
})
|
|
1133
|
+
.catch(() => { });
|
|
1134
|
+
}
|
|
1135
|
+
const { stdout } = await this.bw.runForSession(session, ['get', 'item', id], { timeoutMs: 60_000 });
|
|
1136
|
+
const current = JSON.parse(stdout);
|
|
1137
|
+
// Convert uri match strings to bw enum numbers if needed.
|
|
1138
|
+
const normalizedPatch = deepClone(patch);
|
|
1139
|
+
if (normalizedPatch.login?.uris) {
|
|
1140
|
+
normalizedPatch.login.uris = normalizedPatch.login.uris.map((u) => ({
|
|
1141
|
+
uri: u.uri,
|
|
1142
|
+
match: u.match,
|
|
1143
|
+
}));
|
|
1144
|
+
}
|
|
1145
|
+
const next = applyItemPatch(current, normalizedPatch);
|
|
1146
|
+
// If uris were provided, convert match strings to numbers now.
|
|
1147
|
+
if (patch.login?.uris) {
|
|
1148
|
+
const login = (next.login && typeof next.login === 'object'
|
|
1149
|
+
? next.login
|
|
1150
|
+
: {});
|
|
1151
|
+
login.uris = normalizeUris(patch.login.uris);
|
|
1152
|
+
next.login = login;
|
|
1153
|
+
}
|
|
1154
|
+
const encoded = encodeJsonForBw(next);
|
|
1155
|
+
const { stdout: out } = await this.bw.runForSession(session, ['edit', 'item', id, encoded], { timeoutMs: 120_000 });
|
|
1156
|
+
const updated = JSON.parse(out);
|
|
1157
|
+
if (patch.collectionIds !== undefined) {
|
|
1158
|
+
const encodedCols = encodeJsonForBw(patch.collectionIds);
|
|
1159
|
+
await this.bw
|
|
1160
|
+
.runForSession(session, ['edit', 'item-collections', id, encodedCols], { timeoutMs: 120_000 })
|
|
1161
|
+
.catch(() => { });
|
|
1162
|
+
}
|
|
1163
|
+
return this.maybeRedact(updated, opts.reveal);
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
async setLoginUris(input) {
|
|
1167
|
+
const mode = input.mode ?? 'replace';
|
|
1168
|
+
if (mode !== 'replace' && mode !== 'merge') {
|
|
1169
|
+
throw new Error(`Invalid mode: ${String(mode)}`);
|
|
1170
|
+
}
|
|
1171
|
+
// IMPORTANT: do not call updateItem from inside a bw.withSession callback.
|
|
1172
|
+
// BwSessionManager can serialize session access; nesting can deadlock.
|
|
1173
|
+
const current = await this.bw.withSession(async (session) => {
|
|
1174
|
+
const { stdout } = await this.bw.runForSession(session, ['get', 'item', input.id], { timeoutMs: 60_000 });
|
|
1175
|
+
return JSON.parse(stdout);
|
|
1176
|
+
});
|
|
1177
|
+
const currentLogin = current.login && typeof current.login === 'object'
|
|
1178
|
+
? current.login
|
|
1179
|
+
: null;
|
|
1180
|
+
const existing = denormalizeUris(currentLogin?.uris) ?? [];
|
|
1181
|
+
let nextUris = input.uris;
|
|
1182
|
+
if (mode === 'merge') {
|
|
1183
|
+
const byUri = new Map();
|
|
1184
|
+
for (const u of existing)
|
|
1185
|
+
byUri.set(u.uri, u);
|
|
1186
|
+
for (const u of input.uris)
|
|
1187
|
+
byUri.set(u.uri, u);
|
|
1188
|
+
const seen = new Set();
|
|
1189
|
+
const merged = [];
|
|
1190
|
+
// Existing order first (updated in place if overridden)
|
|
1191
|
+
for (const u of existing) {
|
|
1192
|
+
const v = byUri.get(u.uri);
|
|
1193
|
+
if (!v || seen.has(v.uri))
|
|
1194
|
+
continue;
|
|
1195
|
+
seen.add(v.uri);
|
|
1196
|
+
merged.push(v);
|
|
1197
|
+
}
|
|
1198
|
+
// Append new entries
|
|
1199
|
+
for (const u of input.uris) {
|
|
1200
|
+
if (seen.has(u.uri))
|
|
1201
|
+
continue;
|
|
1202
|
+
seen.add(u.uri);
|
|
1203
|
+
merged.push(u);
|
|
1204
|
+
}
|
|
1205
|
+
nextUris = merged;
|
|
1206
|
+
}
|
|
1207
|
+
return this.updateItem(input.id, { login: { uris: nextUris } }, { reveal: input.reveal });
|
|
1208
|
+
}
|
|
1209
|
+
minimalSummary(item) {
|
|
1210
|
+
if (!item || typeof item !== 'object')
|
|
1211
|
+
return item;
|
|
1212
|
+
const rec = item;
|
|
1213
|
+
return {
|
|
1214
|
+
id: typeof rec.id === 'string' ? rec.id : undefined,
|
|
1215
|
+
name: typeof rec.name === 'string' ? rec.name : undefined,
|
|
1216
|
+
type: kindFromItem(rec),
|
|
1217
|
+
organizationId: typeof rec.organizationId === 'string' ? rec.organizationId : null,
|
|
1218
|
+
folderId: typeof rec.folderId === 'string' ? rec.folderId : null,
|
|
1219
|
+
collectionIds: Array.isArray(rec.collectionIds)
|
|
1220
|
+
? rec.collectionIds
|
|
1221
|
+
: undefined,
|
|
1222
|
+
favorite: typeof rec.favorite === 'boolean' ? rec.favorite : undefined,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
}
|