@red-hat-developer-hub/e2e-test-utils 2.1.14 → 2.1.16
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/dist/deployment/rhdh/constants.d.ts +0 -1
- package/dist/deployment/rhdh/constants.d.ts.map +1 -1
- package/dist/deployment/rhdh/constants.js +0 -1
- package/dist/deployment/rhdh/deployment.d.ts.map +1 -1
- package/dist/deployment/rhdh/deployment.js +0 -3
- package/dist/secrets/bitwarden.d.ts +43 -0
- package/dist/secrets/bitwarden.d.ts.map +1 -1
- package/dist/secrets/bitwarden.js +465 -53
- package/dist/secrets/cli.d.ts +48 -1
- package/dist/secrets/cli.d.ts.map +1 -1
- package/dist/secrets/cli.js +455 -28
- package/dist/secrets/command.d.ts +7 -0
- package/dist/secrets/command.d.ts.map +1 -1
- package/dist/secrets/command.js +103 -26
- package/dist/secrets/config.d.ts +11 -1
- package/dist/secrets/config.d.ts.map +1 -1
- package/dist/secrets/config.js +32 -1
- package/dist/secrets/environment-name.d.ts +2 -0
- package/dist/secrets/environment-name.d.ts.map +1 -0
- package/dist/secrets/environment-name.js +4 -0
- package/dist/secrets/environment.d.ts +10 -0
- package/dist/secrets/environment.d.ts.map +1 -1
- package/dist/secrets/environment.js +35 -4
- package/dist/secrets/exec.d.ts +9 -1
- package/dist/secrets/exec.d.ts.map +1 -1
- package/dist/secrets/exec.js +318 -38
- package/dist/secrets/gsm-wrapper.d.ts +42 -0
- package/dist/secrets/gsm-wrapper.d.ts.map +1 -0
- package/dist/secrets/gsm-wrapper.js +238 -0
- package/dist/secrets/gsm.d.ts +33 -0
- package/dist/secrets/gsm.d.ts.map +1 -0
- package/dist/secrets/gsm.js +172 -0
- package/dist/secrets/index.d.ts +9 -4
- package/dist/secrets/index.d.ts.map +1 -1
- package/dist/secrets/index.js +7 -2
- package/dist/secrets/lock.d.ts +3 -0
- package/dist/secrets/lock.d.ts.map +1 -0
- package/dist/secrets/lock.js +28 -0
- package/dist/secrets/mutation.d.ts +71 -0
- package/dist/secrets/mutation.d.ts.map +1 -0
- package/dist/secrets/mutation.js +210 -0
- package/dist/secrets/secret-input.d.ts +17 -0
- package/dist/secrets/secret-input.d.ts.map +1 -0
- package/dist/secrets/secret-input.js +40 -0
- package/dist/secrets/stream.d.ts +9 -0
- package/dist/secrets/stream.d.ts.map +1 -0
- package/dist/secrets/stream.js +211 -0
- package/dist/secrets/temporary-secret.d.ts +13 -0
- package/dist/secrets/temporary-secret.d.ts.map +1 -0
- package/dist/secrets/temporary-secret.js +87 -0
- package/dist/secrets/tests/bitwarden.test.js +640 -0
- package/dist/secrets/tests/cli.integration.test.js +131 -3
- package/dist/secrets/tests/cli.test.js +281 -6
- package/dist/secrets/tests/command.test.d.ts +2 -0
- package/dist/secrets/tests/command.test.d.ts.map +1 -0
- package/dist/secrets/tests/command.test.js +39 -0
- package/dist/secrets/tests/config.test.js +20 -1
- package/dist/secrets/tests/environment.test.js +145 -3
- package/dist/secrets/tests/exec.test.js +469 -2
- package/dist/secrets/tests/gsm-wrapper.test.d.ts +2 -0
- package/dist/secrets/tests/gsm-wrapper.test.d.ts.map +1 -0
- package/dist/secrets/tests/gsm-wrapper.test.js +166 -0
- package/dist/secrets/tests/gsm.test.d.ts +2 -0
- package/dist/secrets/tests/gsm.test.d.ts.map +1 -0
- package/dist/secrets/tests/gsm.test.js +181 -0
- package/dist/secrets/tests/mutation.test.d.ts +2 -0
- package/dist/secrets/tests/mutation.test.d.ts.map +1 -0
- package/dist/secrets/tests/mutation.test.js +390 -0
- package/dist/secrets/tests/secret-input.test.d.ts +2 -0
- package/dist/secrets/tests/secret-input.test.d.ts.map +1 -0
- package/dist/secrets/tests/secret-input.test.js +71 -0
- package/dist/secrets/tests/stream.test.d.ts +2 -0
- package/dist/secrets/tests/stream.test.d.ts.map +1 -0
- package/dist/secrets/tests/stream.test.js +88 -0
- package/dist/secrets/tests/temporary-secret.test.d.ts +2 -0
- package/dist/secrets/tests/temporary-secret.test.d.ts.map +1 -0
- package/dist/secrets/tests/temporary-secret.test.js +76 -0
- package/package.json +1 -1
- package/dist/deployment/rhdh/config/new-frontend-system/secrets.yaml +0 -8
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
import { runCommand } from "./command.js";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import { getCollectionMapping, } from "./config.js";
|
|
5
|
+
import { createTemporarySecretFile } from "./temporary-secret.js";
|
|
3
6
|
export class BitwardenClient {
|
|
4
7
|
command;
|
|
5
8
|
env;
|
|
6
9
|
runner;
|
|
10
|
+
removeTemporaryDirectory;
|
|
11
|
+
sessionCheck;
|
|
12
|
+
collections = new Map();
|
|
13
|
+
itemCollections = new Map();
|
|
7
14
|
constructor(options = {}) {
|
|
8
15
|
this.command = options.command ?? "bw";
|
|
9
16
|
this.env = { ...process.env, ...options.env };
|
|
10
17
|
this.runner = options.runner ?? runCommand;
|
|
18
|
+
this.removeTemporaryDirectory =
|
|
19
|
+
options.removeTemporaryDirectory ??
|
|
20
|
+
((directory) => rm(directory, { recursive: true, force: true }));
|
|
11
21
|
}
|
|
12
22
|
async read(collectionId, selectors) {
|
|
13
23
|
const mapping = getCollectionMapping(collectionId);
|
|
14
|
-
|
|
15
|
-
throw new Error("BW_SESSION is required and must contain an unlocked Bitwarden session");
|
|
16
|
-
}
|
|
17
|
-
await this.runOrThrow(["--version"], "Bitwarden CLI is unavailable");
|
|
18
|
-
const status = await this.runOrThrow(["status"], "Bitwarden session status could not be checked");
|
|
19
|
-
const statusJson = parseJson(status, "Bitwarden status");
|
|
20
|
-
if (!isRecord(statusJson) || statusJson.status !== "unlocked") {
|
|
21
|
-
throw new Error("BW_SESSION is missing or Bitwarden is not unlocked");
|
|
22
|
-
}
|
|
24
|
+
await this.ensureSession();
|
|
23
25
|
await this.runOrThrow(["sync"], "Bitwarden sync failed");
|
|
24
26
|
const collection = await this.resolveCollection(mapping.bitwardenCollection);
|
|
25
27
|
const result = [];
|
|
@@ -39,7 +41,133 @@ export class BitwardenClient {
|
|
|
39
41
|
}
|
|
40
42
|
return result;
|
|
41
43
|
}
|
|
44
|
+
async readItem(collectionId, name) {
|
|
45
|
+
const item = await this.findItem(collectionId, name);
|
|
46
|
+
if (!item)
|
|
47
|
+
throw new Error(`Bitwarden item not found: ${name}`);
|
|
48
|
+
return item;
|
|
49
|
+
}
|
|
50
|
+
async findItem(collectionId, name) {
|
|
51
|
+
const mapping = getCollectionMapping(collectionId);
|
|
52
|
+
await this.ensureSession();
|
|
53
|
+
await this.runOrThrow(["sync"], "Bitwarden sync failed");
|
|
54
|
+
const collection = await this.resolveCollection(mapping.bitwardenCollection);
|
|
55
|
+
const listed = await this.runOrThrow(["list", "items", "--collectionid", collection.id, "--search", name], `Bitwarden item listing failed for ${name}`);
|
|
56
|
+
const values = parseJson(listed, `Bitwarden item list for ${name}`);
|
|
57
|
+
if (!Array.isArray(values)) {
|
|
58
|
+
throw new Error(`Bitwarden returned an invalid item list for ${name}`);
|
|
59
|
+
}
|
|
60
|
+
const matches = [];
|
|
61
|
+
for (const value of values) {
|
|
62
|
+
if (isRecord(value) &&
|
|
63
|
+
typeof value.name === "string" &&
|
|
64
|
+
value.name !== name)
|
|
65
|
+
continue;
|
|
66
|
+
if (!isRecord(value) || typeof value.id !== "string" || !value.id) {
|
|
67
|
+
throw new Error(`Bitwarden returned an item without an id for ${name}`);
|
|
68
|
+
}
|
|
69
|
+
const item = isCompleteItem(value, true)
|
|
70
|
+
? await this.parseItem(value, value.id, collection, name, true)
|
|
71
|
+
: await this.readItemById(value.id, collection, name);
|
|
72
|
+
if (item.name === name)
|
|
73
|
+
matches.push(item);
|
|
74
|
+
}
|
|
75
|
+
if (matches.length === 0)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (matches.length > 1)
|
|
78
|
+
throw new Error(`Bitwarden item name is ambiguous: ${name}`);
|
|
79
|
+
return matches[0];
|
|
80
|
+
}
|
|
81
|
+
async createItem(collectionId, name, value, storage) {
|
|
82
|
+
const mapping = getCollectionMapping(collectionId);
|
|
83
|
+
await this.ensureSession();
|
|
84
|
+
await this.runOrThrow(["sync"], "Bitwarden sync failed");
|
|
85
|
+
const collection = await this.resolveCollection(mapping.bitwardenCollection);
|
|
86
|
+
const payload = this.itemPayload({
|
|
87
|
+
name,
|
|
88
|
+
collection,
|
|
89
|
+
notes: storage === "note" ? value : null,
|
|
90
|
+
attachments: [],
|
|
91
|
+
});
|
|
92
|
+
const encoded = this.encodeItem(payload);
|
|
93
|
+
const created = await this.runOrThrow(["create", "item"], `Bitwarden item creation failed for ${name}`, encoded);
|
|
94
|
+
const createdValue = parseJson(created, `Bitwarden item creation for ${name}`);
|
|
95
|
+
const createdId = isRecord(createdValue) && typeof createdValue.id === "string"
|
|
96
|
+
? createdValue.id
|
|
97
|
+
: undefined;
|
|
98
|
+
if (!createdId) {
|
|
99
|
+
throw new Error(`Bitwarden item creation returned no id: ${name}`);
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
if (storage === "attachment") {
|
|
103
|
+
await this.createAttachment(createdId, attachmentFileName(name), value, name);
|
|
104
|
+
}
|
|
105
|
+
const verified = storage === "note" &&
|
|
106
|
+
isRecord(createdValue) &&
|
|
107
|
+
isCompleteItem(createdValue, true)
|
|
108
|
+
? await this.parseItem(createdValue, createdId, collection, name, true)
|
|
109
|
+
: await this.refreshItemById(createdId, collection, name);
|
|
110
|
+
if (verified.value !== value || verified.storage !== storage) {
|
|
111
|
+
throw new Error(`Bitwarden read-back verification failed: ${name}`);
|
|
112
|
+
}
|
|
113
|
+
return verified;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
try {
|
|
117
|
+
await this.deleteItemById(createdId, name);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw new Error(`Bitwarden item creation failed and cleanup failed: ${name}`, { cause: error });
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async deleteItem(item) {
|
|
126
|
+
await this.ensureSession();
|
|
127
|
+
const collection = this.collectionForItem(item);
|
|
128
|
+
await this.deleteItemById(item.id, item.name);
|
|
129
|
+
await this.runOrThrow(["sync"], "Bitwarden sync failed");
|
|
130
|
+
if (await this.findItemInCollection(collection, item.name)) {
|
|
131
|
+
throw new Error(`Bitwarden item deletion could not be verified: ${item.name}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async updateItem(item, value, storage = item.storage) {
|
|
135
|
+
const current = await this.refreshItem(item);
|
|
136
|
+
if (current.revisionDate !== item.revisionDate) {
|
|
137
|
+
throw new Error(`Bitwarden item changed before update: ${item.name}`);
|
|
138
|
+
}
|
|
139
|
+
item = current;
|
|
140
|
+
if (item.value === value && item.storage === storage)
|
|
141
|
+
return item;
|
|
142
|
+
if (item.storage === storage && item.storage === "note") {
|
|
143
|
+
const edited = await this.editItem(item.id, this.updatedItemPayload(item, value, "note"), item.name);
|
|
144
|
+
const updatedValue = parseJson(edited, `Bitwarden update for ${item.name}`);
|
|
145
|
+
const updated = await this.parseItem(updatedValue, item.id, this.collectionForItem(item), item.name, true);
|
|
146
|
+
if (updated.value !== value || updated.storage !== "note") {
|
|
147
|
+
throw new Error(`Bitwarden read-back verification failed: ${item.name}`);
|
|
148
|
+
}
|
|
149
|
+
return updated;
|
|
150
|
+
}
|
|
151
|
+
if (item.storage === storage && item.storage === "attachment") {
|
|
152
|
+
return this.updateAttachment(item, value);
|
|
153
|
+
}
|
|
154
|
+
if (storage === "attachment") {
|
|
155
|
+
return this.convertNoteToAttachment(item, value);
|
|
156
|
+
}
|
|
157
|
+
return this.convertAttachmentToNote(item, value);
|
|
158
|
+
}
|
|
42
159
|
async resolveCollection(collectionName) {
|
|
160
|
+
const cached = this.collections.get(collectionName);
|
|
161
|
+
if (cached)
|
|
162
|
+
return cached;
|
|
163
|
+
const pending = this.loadCollection(collectionName).catch((error) => {
|
|
164
|
+
this.collections.delete(collectionName);
|
|
165
|
+
throw error;
|
|
166
|
+
});
|
|
167
|
+
this.collections.set(collectionName, pending);
|
|
168
|
+
return pending;
|
|
169
|
+
}
|
|
170
|
+
async loadCollection(collectionName) {
|
|
43
171
|
const result = await this.runOrThrow(["list", "collections"], "Bitwarden collection listing failed");
|
|
44
172
|
const values = parseJson(result, "Bitwarden collection list");
|
|
45
173
|
if (!Array.isArray(values)) {
|
|
@@ -77,7 +205,7 @@ export class BitwardenClient {
|
|
|
77
205
|
if (!Array.isArray(values)) {
|
|
78
206
|
throw new Error(`Bitwarden returned an invalid item list for ${selector.prefix}`);
|
|
79
207
|
}
|
|
80
|
-
const
|
|
208
|
+
const listedItems = values.flatMap((value) => {
|
|
81
209
|
if (!isRecord(value) ||
|
|
82
210
|
typeof value.id !== "string" ||
|
|
83
211
|
value.id.length === 0) {
|
|
@@ -87,64 +215,339 @@ export class BitwardenClient {
|
|
|
87
215
|
!value.name.startsWith(selector.prefix)) {
|
|
88
216
|
return [];
|
|
89
217
|
}
|
|
90
|
-
return [value.id];
|
|
218
|
+
return [{ id: value.id, value }];
|
|
91
219
|
});
|
|
92
|
-
if (new Set(
|
|
220
|
+
if (new Set(listedItems.map(({ id }) => id)).size !== listedItems.length) {
|
|
93
221
|
throw new Error(`Duplicate Bitwarden item id for ${selector.prefix}`);
|
|
94
222
|
}
|
|
95
223
|
const items = [];
|
|
96
|
-
for (const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
if (typeof value.name !== "string" ||
|
|
103
|
-
!value.name.startsWith(selector.prefix)) {
|
|
224
|
+
for (const listed of listedItems) {
|
|
225
|
+
const item = isCompleteItem(listed.value, false)
|
|
226
|
+
? await this.parseItem(listed.value, listed.id, collection, selector.prefix, false)
|
|
227
|
+
: await this.readItemById(listed.id, collection, selector.prefix, false);
|
|
228
|
+
if (!item.name.startsWith(selector.prefix))
|
|
104
229
|
continue;
|
|
230
|
+
items.push({
|
|
231
|
+
id: item.id,
|
|
232
|
+
name: item.name,
|
|
233
|
+
value: item.value,
|
|
234
|
+
selector,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return items;
|
|
238
|
+
}
|
|
239
|
+
async ensureSession() {
|
|
240
|
+
if (!this.env.BW_SESSION?.trim()) {
|
|
241
|
+
throw new Error("BW_SESSION is required and must contain an unlocked Bitwarden session");
|
|
242
|
+
}
|
|
243
|
+
if (!this.sessionCheck) {
|
|
244
|
+
this.sessionCheck = this.checkSession().catch((error) => {
|
|
245
|
+
this.sessionCheck = undefined;
|
|
246
|
+
throw error;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
await this.sessionCheck;
|
|
250
|
+
}
|
|
251
|
+
async checkSession() {
|
|
252
|
+
await this.runOrThrow(["--version"], "Bitwarden CLI is unavailable");
|
|
253
|
+
const status = await this.runOrThrow(["status"], "Bitwarden session status could not be checked");
|
|
254
|
+
const statusJson = parseJson(status, "Bitwarden status");
|
|
255
|
+
if (!isRecord(statusJson) || statusJson.status !== "unlocked") {
|
|
256
|
+
throw new Error("BW_SESSION is missing or Bitwarden is not unlocked");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async readItemById(id, collection, label, requireRevision = true) {
|
|
260
|
+
const result = await this.runOrThrow(["get", "item", id], `Bitwarden item read failed for ${label}`);
|
|
261
|
+
return this.parseItem(parseJson(result, `Bitwarden item ${id}`), id, collection, label, requireRevision);
|
|
262
|
+
}
|
|
263
|
+
async parseItem(input, id, collection, label, requireRevision) {
|
|
264
|
+
const value = input;
|
|
265
|
+
if (!isRecord(value)) {
|
|
266
|
+
throw new Error(`Bitwarden returned an invalid item for ${label}`);
|
|
267
|
+
}
|
|
268
|
+
if (value.id !== id ||
|
|
269
|
+
typeof value.name !== "string" ||
|
|
270
|
+
value.type !== 2 ||
|
|
271
|
+
!Array.isArray(value.collectionIds) ||
|
|
272
|
+
value.collectionIds.length !== 1 ||
|
|
273
|
+
value.collectionIds[0] !== collection.id ||
|
|
274
|
+
value.organizationId !== collection.organizationId ||
|
|
275
|
+
(requireRevision &&
|
|
276
|
+
(typeof value.revisionDate !== "string" ||
|
|
277
|
+
value.revisionDate.length === 0))) {
|
|
278
|
+
throw new Error(`Bitwarden item ${typeof value.name === "string" ? value.name : id} is not a valid secure note in the selected collection`);
|
|
279
|
+
}
|
|
280
|
+
const attachment = parseAttachment(value.attachments, label);
|
|
281
|
+
if (attachment === undefined) {
|
|
282
|
+
if (typeof value.notes !== "string") {
|
|
283
|
+
throw new Error(`Bitwarden secure note has no value: ${value.name}`);
|
|
105
284
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
value.
|
|
110
|
-
|
|
111
|
-
value.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
285
|
+
const item = {
|
|
286
|
+
id,
|
|
287
|
+
name: value.name,
|
|
288
|
+
value: value.notes,
|
|
289
|
+
storage: "note",
|
|
290
|
+
revisionDate: typeof value.revisionDate === "string" ? value.revisionDate : "",
|
|
291
|
+
raw: value,
|
|
292
|
+
attachments: [],
|
|
293
|
+
};
|
|
294
|
+
this.itemCollections.set(id, collection);
|
|
295
|
+
return item;
|
|
296
|
+
}
|
|
297
|
+
if (value.notes !== null &&
|
|
298
|
+
(typeof value.notes !== "string" || value.notes.length > 0)) {
|
|
299
|
+
throw new Error(requireRevision
|
|
300
|
+
? `Bitwarden item notes must be empty with an attachment: ${value.name}`
|
|
301
|
+
: `Bitwarden item notes must be null or empty when an attachment is present for ${label}`);
|
|
302
|
+
}
|
|
303
|
+
if (attachment.fileName !== attachmentFileName(value.name)) {
|
|
304
|
+
throw new Error(`Bitwarden attachment filename does not match the item basename: ${value.name}`);
|
|
305
|
+
}
|
|
306
|
+
const attachmentResult = await this.runOrThrow(["get", "attachment", attachment.fileName, "--itemid", id, "--raw"], `Bitwarden attachment read failed for ${label}`);
|
|
307
|
+
const item = {
|
|
308
|
+
id,
|
|
309
|
+
name: value.name,
|
|
310
|
+
value: attachmentResult.stdout,
|
|
311
|
+
storage: "attachment",
|
|
312
|
+
revisionDate: typeof value.revisionDate === "string" ? value.revisionDate : "",
|
|
313
|
+
raw: value,
|
|
314
|
+
attachments: [attachment],
|
|
315
|
+
attachment,
|
|
316
|
+
};
|
|
317
|
+
this.itemCollections.set(id, collection);
|
|
318
|
+
return item;
|
|
319
|
+
}
|
|
320
|
+
async refreshItem(item) {
|
|
321
|
+
await this.runOrThrow(["sync"], "Bitwarden sync failed");
|
|
322
|
+
return this.readItemById(item.id, this.collectionForItem(item), item.name);
|
|
323
|
+
}
|
|
324
|
+
async refreshItemById(id, collection, name) {
|
|
325
|
+
await this.runOrThrow(["sync"], "Bitwarden sync failed");
|
|
326
|
+
return this.readItemById(id, collection, name);
|
|
327
|
+
}
|
|
328
|
+
async findItemInCollection(collection, name) {
|
|
329
|
+
const listed = await this.runOrThrow(["list", "items", "--collectionid", collection.id, "--search", name], `Bitwarden item listing failed for ${name}`);
|
|
330
|
+
const values = parseJson(listed, `Bitwarden item list for ${name}`);
|
|
331
|
+
if (!Array.isArray(values)) {
|
|
332
|
+
throw new Error(`Bitwarden returned an invalid item list for ${name}`);
|
|
333
|
+
}
|
|
334
|
+
for (const value of values) {
|
|
335
|
+
if (isRecord(value) &&
|
|
336
|
+
typeof value.name === "string" &&
|
|
337
|
+
value.name !== name)
|
|
125
338
|
continue;
|
|
339
|
+
if (!isRecord(value) || typeof value.id !== "string" || !value.id) {
|
|
340
|
+
throw new Error(`Bitwarden returned an item without an id for ${name}`);
|
|
126
341
|
}
|
|
127
|
-
|
|
128
|
-
(
|
|
129
|
-
|
|
342
|
+
const item = isCompleteItem(value, true)
|
|
343
|
+
? await this.parseItem(value, value.id, collection, name, true)
|
|
344
|
+
: await this.readItemById(value.id, collection, name);
|
|
345
|
+
if (item.name === name)
|
|
346
|
+
return item;
|
|
347
|
+
}
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
collectionForItem(item) {
|
|
351
|
+
const collection = this.itemCollections.get(item.id);
|
|
352
|
+
if (!collection) {
|
|
353
|
+
throw new Error("Bitwarden collection must be loaded before changing an item");
|
|
354
|
+
}
|
|
355
|
+
return collection;
|
|
356
|
+
}
|
|
357
|
+
async deleteItemById(id, name) {
|
|
358
|
+
await this.runOrThrow(["delete", "item", id], `Bitwarden item deletion failed for ${name}`);
|
|
359
|
+
}
|
|
360
|
+
itemPayload(options) {
|
|
361
|
+
return {
|
|
362
|
+
type: 2,
|
|
363
|
+
name: options.name,
|
|
364
|
+
notes: options.notes,
|
|
365
|
+
attachments: options.attachments,
|
|
366
|
+
collectionIds: [options.collection.id],
|
|
367
|
+
organizationId: options.collection.organizationId,
|
|
368
|
+
secureNote: { type: 0 },
|
|
369
|
+
login: null,
|
|
370
|
+
card: null,
|
|
371
|
+
identity: null,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
updatedItemPayload(item, value, storage) {
|
|
375
|
+
return {
|
|
376
|
+
...item.raw,
|
|
377
|
+
name: item.name,
|
|
378
|
+
notes: storage === "note" ? value : null,
|
|
379
|
+
attachments: [],
|
|
380
|
+
type: 2,
|
|
381
|
+
collectionIds: item.raw.collectionIds,
|
|
382
|
+
organizationId: item.raw.organizationId,
|
|
383
|
+
secureNote: { type: 0 },
|
|
384
|
+
login: null,
|
|
385
|
+
card: null,
|
|
386
|
+
identity: null,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
encodeItem(payload) {
|
|
390
|
+
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
|
391
|
+
}
|
|
392
|
+
async updateAttachment(item, value) {
|
|
393
|
+
if (!item.attachment) {
|
|
394
|
+
throw new Error(`Bitwarden attachment metadata missing: ${item.name}`);
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
await this.runOrThrow(["delete", "attachment", item.attachment.id], `Bitwarden attachment update failed for ${item.name}`);
|
|
398
|
+
await this.createAttachment(item.id, item.attachment.fileName, value, item.name);
|
|
399
|
+
const updated = await this.refreshItem(item);
|
|
400
|
+
if (updated.value !== value) {
|
|
401
|
+
throw new Error(`Bitwarden read-back verification failed: ${item.name}`);
|
|
130
402
|
}
|
|
131
|
-
|
|
132
|
-
|
|
403
|
+
return updated;
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
await this.restoreAttachment(item);
|
|
407
|
+
if (error instanceof Error &&
|
|
408
|
+
/read-back verification failed/.test(error.message)) {
|
|
409
|
+
throw error;
|
|
133
410
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
id,
|
|
137
|
-
name: value.name,
|
|
138
|
-
value: attachmentResult.stdout,
|
|
139
|
-
selector,
|
|
411
|
+
throw new Error(`Bitwarden attachment update failed: ${item.name}`, {
|
|
412
|
+
cause: error,
|
|
140
413
|
});
|
|
141
414
|
}
|
|
142
|
-
return items;
|
|
143
415
|
}
|
|
144
|
-
async
|
|
416
|
+
async convertNoteToAttachment(item, value) {
|
|
417
|
+
let attachmentUploaded = false;
|
|
418
|
+
try {
|
|
419
|
+
await this.editItem(item.id, this.updatedItemPayload(item, value, "attachment"), item.name);
|
|
420
|
+
await this.createAttachment(item.id, attachmentFileName(item.name), value, item.name);
|
|
421
|
+
attachmentUploaded = true;
|
|
422
|
+
const updated = await this.refreshItem(item);
|
|
423
|
+
if (updated.storage !== "attachment" || updated.value !== value) {
|
|
424
|
+
throw new Error(`Bitwarden read-back verification failed: ${item.name}`);
|
|
425
|
+
}
|
|
426
|
+
return updated;
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
let attachmentCleanupError;
|
|
430
|
+
if (attachmentUploaded) {
|
|
431
|
+
try {
|
|
432
|
+
await this.removeUploadedAttachment(item);
|
|
433
|
+
}
|
|
434
|
+
catch (cleanupError) {
|
|
435
|
+
attachmentCleanupError = cleanupError;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
await this.restoreNote(item);
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
throw new Error(`Bitwarden update failed and note rollback failed: ${item.name}`, { cause: error });
|
|
443
|
+
}
|
|
444
|
+
if (attachmentCleanupError !== undefined) {
|
|
445
|
+
throw new Error(`Bitwarden update failed and attachment rollback failed: ${item.name}`, { cause: error });
|
|
446
|
+
}
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
async removeUploadedAttachment(item) {
|
|
451
|
+
const attachment = (await this.readAttachmentMetadata(item)).find(({ fileName }) => fileName === attachmentFileName(item.name));
|
|
452
|
+
if (!attachment)
|
|
453
|
+
return;
|
|
454
|
+
await this.runOrThrow(["delete", "attachment", attachment.id], `Bitwarden attachment rollback failed for ${item.name}`);
|
|
455
|
+
}
|
|
456
|
+
async convertAttachmentToNote(item, value) {
|
|
457
|
+
if (!item.attachment) {
|
|
458
|
+
throw new Error(`Bitwarden attachment metadata missing: ${item.name}`);
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
await this.runOrThrow(["delete", "attachment", item.attachment.id], `Bitwarden attachment update failed for ${item.name}`);
|
|
462
|
+
await this.editItem(item.id, this.updatedItemPayload(item, value, "note"), item.name);
|
|
463
|
+
const updated = await this.refreshItem(item);
|
|
464
|
+
if (updated.storage !== "note" || updated.value !== value) {
|
|
465
|
+
throw new Error(`Bitwarden read-back verification failed: ${item.name}`);
|
|
466
|
+
}
|
|
467
|
+
return updated;
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
await this.restoreAttachment(item);
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async restoreNote(item) {
|
|
475
|
+
try {
|
|
476
|
+
await this.editItem(item.id, this.updatedItemPayload(item, item.value, "note"), item.name);
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
throw new Error(`Bitwarden update failed and note rollback failed: ${item.name}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async restoreAttachment(item) {
|
|
483
|
+
if (!item.attachment) {
|
|
484
|
+
throw new Error(`Bitwarden attachment metadata missing: ${item.name}`);
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
await this.editItem(item.id, this.updatedItemPayload(item, item.value, "attachment"), item.name);
|
|
488
|
+
const attachments = await this.readAttachmentMetadata(item);
|
|
489
|
+
for (const attachment of attachments) {
|
|
490
|
+
if (attachment.fileName === item.attachment.fileName) {
|
|
491
|
+
await this.runOrThrow(["delete", "attachment", attachment.id], `Bitwarden attachment rollback failed for ${item.name}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
await this.createAttachment(item.id, item.attachment.fileName, item.value, item.name);
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
throw new Error(`Bitwarden update failed and attachment rollback failed: ${item.name}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async readAttachmentMetadata(item) {
|
|
501
|
+
const result = await this.runOrThrow(["get", "item", item.id], `Bitwarden item read failed for ${item.name}`);
|
|
502
|
+
const value = parseJson(result, `Bitwarden item ${item.id}`);
|
|
503
|
+
if (!isRecord(value) || value.id !== item.id) {
|
|
504
|
+
throw new Error(`Bitwarden item read-back failed: ${item.name}`);
|
|
505
|
+
}
|
|
506
|
+
const attachment = parseAttachment(value.attachments, item.name);
|
|
507
|
+
return attachment === undefined ? [] : [attachment];
|
|
508
|
+
}
|
|
509
|
+
async editItem(id, payload, name) {
|
|
510
|
+
const encoded = this.encodeItem(payload);
|
|
511
|
+
return this.runOrThrow(["edit", "item", id], `Bitwarden update failed for ${name}`, encoded);
|
|
512
|
+
}
|
|
513
|
+
async createAttachment(itemId, fileName, value, name) {
|
|
514
|
+
if (path.basename(fileName) !== fileName) {
|
|
515
|
+
throw new Error(`Bitwarden attachment filename is unsafe: ${name}`);
|
|
516
|
+
}
|
|
517
|
+
const temporary = await createTemporarySecretFile(value, fileName, {
|
|
518
|
+
removeDirectory: this.removeTemporaryDirectory,
|
|
519
|
+
});
|
|
520
|
+
let operationError;
|
|
521
|
+
try {
|
|
522
|
+
await this.runOrThrow(["create", "attachment", "--file", temporary.path, "--itemid", itemId], `Bitwarden attachment update failed for ${name}`);
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
operationError = error;
|
|
526
|
+
}
|
|
527
|
+
let cleanupError;
|
|
528
|
+
try {
|
|
529
|
+
await temporary.remove();
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
cleanupError = error;
|
|
533
|
+
}
|
|
534
|
+
if (operationError !== undefined) {
|
|
535
|
+
if (cleanupError !== undefined) {
|
|
536
|
+
throw new Error(`Bitwarden attachment operation failed and temporary cleanup failed: ${name}`, { cause: new AggregateError([operationError, cleanupError]) });
|
|
537
|
+
}
|
|
538
|
+
throw operationError;
|
|
539
|
+
}
|
|
540
|
+
if (cleanupError !== undefined) {
|
|
541
|
+
throw new Error(`Bitwarden attachment operation may have succeeded but temporary cleanup failed: ${name}`, { cause: cleanupError });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async runOrThrow(args, message, input) {
|
|
145
545
|
let result;
|
|
146
546
|
try {
|
|
147
|
-
result = await this.runner(this.command, args, {
|
|
547
|
+
result = await this.runner(this.command, args, {
|
|
548
|
+
env: this.env,
|
|
549
|
+
...(input === undefined ? {} : { input }),
|
|
550
|
+
});
|
|
148
551
|
}
|
|
149
552
|
catch {
|
|
150
553
|
throw new Error(message);
|
|
@@ -165,6 +568,15 @@ function parseJson(result, label) {
|
|
|
165
568
|
function isRecord(value) {
|
|
166
569
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
167
570
|
}
|
|
571
|
+
function isCompleteItem(value, requireRevision) {
|
|
572
|
+
return (typeof value.id === "string" &&
|
|
573
|
+
"name" in value &&
|
|
574
|
+
"type" in value &&
|
|
575
|
+
"collectionIds" in value &&
|
|
576
|
+
"organizationId" in value &&
|
|
577
|
+
"notes" in value &&
|
|
578
|
+
(!requireRevision || "revisionDate" in value));
|
|
579
|
+
}
|
|
168
580
|
function parseAttachment(value, prefix) {
|
|
169
581
|
if (value === undefined || value === null)
|
|
170
582
|
return undefined;
|
package/dist/secrets/cli.d.ts
CHANGED
|
@@ -1,11 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { type MutationPlan } from "./mutation.js";
|
|
2
3
|
export interface ExecCliArguments {
|
|
3
4
|
command: "exec";
|
|
4
5
|
profilePath: string;
|
|
5
6
|
workspaces: string[];
|
|
6
7
|
executable: string;
|
|
7
8
|
args: string[];
|
|
9
|
+
streamSecrets?: boolean;
|
|
8
10
|
}
|
|
9
|
-
export
|
|
11
|
+
export type HelpTopic = "root" | "exec" | "create" | "update" | "delete" | "describe" | "list" | "gsm-login" | "gsm-clean";
|
|
12
|
+
export interface HelpCliArguments {
|
|
13
|
+
command: "help";
|
|
14
|
+
topic: HelpTopic;
|
|
15
|
+
}
|
|
16
|
+
interface MutationCliBase {
|
|
17
|
+
collection: string;
|
|
18
|
+
bitwardenPath: string;
|
|
19
|
+
dryRun: boolean;
|
|
20
|
+
gsmTimeoutMs?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface CreateCliArguments extends MutationCliBase {
|
|
23
|
+
command: "create";
|
|
24
|
+
fromFile?: string;
|
|
25
|
+
fromStdin: boolean;
|
|
26
|
+
allowEmpty: boolean;
|
|
27
|
+
force: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface UpdateCliArguments extends MutationCliBase {
|
|
30
|
+
command: "update";
|
|
31
|
+
fromFile?: string;
|
|
32
|
+
fromStdin: boolean;
|
|
33
|
+
allowEmpty: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface DeleteCliArguments extends MutationCliBase {
|
|
36
|
+
command: "delete";
|
|
37
|
+
force: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface DescribeCliArguments {
|
|
40
|
+
command: "describe";
|
|
41
|
+
collection: string;
|
|
42
|
+
bitwardenPath: string;
|
|
43
|
+
output: "text" | "json";
|
|
44
|
+
}
|
|
45
|
+
export interface ListCliArguments {
|
|
46
|
+
command: "list";
|
|
47
|
+
collection?: string;
|
|
48
|
+
output: "text" | "json";
|
|
49
|
+
}
|
|
50
|
+
export type CliArguments = HelpCliArguments | ExecCliArguments | CreateCliArguments | UpdateCliArguments | DeleteCliArguments | DescribeCliArguments | ListCliArguments | {
|
|
51
|
+
command: "gsm-login" | "gsm-clean";
|
|
52
|
+
};
|
|
53
|
+
export declare function parseCliArguments(argv: readonly string[]): CliArguments;
|
|
54
|
+
export declare function getHelpText(topic: HelpTopic): string;
|
|
10
55
|
export declare function main(argv?: readonly string[]): Promise<number>;
|
|
56
|
+
export declare function formatMutationResult(state: "dry-run" | "applied", plan: MutationPlan): string;
|
|
57
|
+
export {};
|
|
11
58
|
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/secrets/cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/secrets/cli.ts"],"names":[],"mappings":";AAeA,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,MAAM,SAAS,GACjB,MAAM,GACN,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,MAAM,GACN,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,SAAS,CAAC;CAClB;AAED,UAAU,eAAe;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,OAAO,EAAE,QAAQ,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,OAAO,EAAE,QAAQ,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,OAAO,EAAE,QAAQ,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,UAAU,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,YAAY,GACpB,gBAAgB,GAChB,gBAAgB,GAChB,kBAAkB,GAClB,kBAAkB,GAClB,kBAAkB,GAClB,oBAAoB,GACpB,gBAAgB,GAChB;IAAE,OAAO,EAAE,WAAW,GAAG,WAAW,CAAA;CAAE,CAAC;AA6F3C,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,YAAY,CAgBvE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAEpD;AA2SD,wBAAsB,IAAI,CACxB,IAAI,GAAE,SAAS,MAAM,EAA0B,GAC9C,OAAO,CAAC,MAAM,CAAC,CAoFjB;AAoBD,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,IAAI,EAAE,YAAY,GACjB,MAAM,CASR"}
|