@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.
Files changed (79) hide show
  1. package/dist/deployment/rhdh/constants.d.ts +0 -1
  2. package/dist/deployment/rhdh/constants.d.ts.map +1 -1
  3. package/dist/deployment/rhdh/constants.js +0 -1
  4. package/dist/deployment/rhdh/deployment.d.ts.map +1 -1
  5. package/dist/deployment/rhdh/deployment.js +0 -3
  6. package/dist/secrets/bitwarden.d.ts +43 -0
  7. package/dist/secrets/bitwarden.d.ts.map +1 -1
  8. package/dist/secrets/bitwarden.js +465 -53
  9. package/dist/secrets/cli.d.ts +48 -1
  10. package/dist/secrets/cli.d.ts.map +1 -1
  11. package/dist/secrets/cli.js +455 -28
  12. package/dist/secrets/command.d.ts +7 -0
  13. package/dist/secrets/command.d.ts.map +1 -1
  14. package/dist/secrets/command.js +103 -26
  15. package/dist/secrets/config.d.ts +11 -1
  16. package/dist/secrets/config.d.ts.map +1 -1
  17. package/dist/secrets/config.js +32 -1
  18. package/dist/secrets/environment-name.d.ts +2 -0
  19. package/dist/secrets/environment-name.d.ts.map +1 -0
  20. package/dist/secrets/environment-name.js +4 -0
  21. package/dist/secrets/environment.d.ts +10 -0
  22. package/dist/secrets/environment.d.ts.map +1 -1
  23. package/dist/secrets/environment.js +35 -4
  24. package/dist/secrets/exec.d.ts +9 -1
  25. package/dist/secrets/exec.d.ts.map +1 -1
  26. package/dist/secrets/exec.js +318 -38
  27. package/dist/secrets/gsm-wrapper.d.ts +42 -0
  28. package/dist/secrets/gsm-wrapper.d.ts.map +1 -0
  29. package/dist/secrets/gsm-wrapper.js +238 -0
  30. package/dist/secrets/gsm.d.ts +33 -0
  31. package/dist/secrets/gsm.d.ts.map +1 -0
  32. package/dist/secrets/gsm.js +172 -0
  33. package/dist/secrets/index.d.ts +9 -4
  34. package/dist/secrets/index.d.ts.map +1 -1
  35. package/dist/secrets/index.js +7 -2
  36. package/dist/secrets/lock.d.ts +3 -0
  37. package/dist/secrets/lock.d.ts.map +1 -0
  38. package/dist/secrets/lock.js +28 -0
  39. package/dist/secrets/mutation.d.ts +71 -0
  40. package/dist/secrets/mutation.d.ts.map +1 -0
  41. package/dist/secrets/mutation.js +210 -0
  42. package/dist/secrets/secret-input.d.ts +17 -0
  43. package/dist/secrets/secret-input.d.ts.map +1 -0
  44. package/dist/secrets/secret-input.js +40 -0
  45. package/dist/secrets/stream.d.ts +9 -0
  46. package/dist/secrets/stream.d.ts.map +1 -0
  47. package/dist/secrets/stream.js +211 -0
  48. package/dist/secrets/temporary-secret.d.ts +13 -0
  49. package/dist/secrets/temporary-secret.d.ts.map +1 -0
  50. package/dist/secrets/temporary-secret.js +87 -0
  51. package/dist/secrets/tests/bitwarden.test.js +640 -0
  52. package/dist/secrets/tests/cli.integration.test.js +131 -3
  53. package/dist/secrets/tests/cli.test.js +281 -6
  54. package/dist/secrets/tests/command.test.d.ts +2 -0
  55. package/dist/secrets/tests/command.test.d.ts.map +1 -0
  56. package/dist/secrets/tests/command.test.js +39 -0
  57. package/dist/secrets/tests/config.test.js +20 -1
  58. package/dist/secrets/tests/environment.test.js +145 -3
  59. package/dist/secrets/tests/exec.test.js +469 -2
  60. package/dist/secrets/tests/gsm-wrapper.test.d.ts +2 -0
  61. package/dist/secrets/tests/gsm-wrapper.test.d.ts.map +1 -0
  62. package/dist/secrets/tests/gsm-wrapper.test.js +166 -0
  63. package/dist/secrets/tests/gsm.test.d.ts +2 -0
  64. package/dist/secrets/tests/gsm.test.d.ts.map +1 -0
  65. package/dist/secrets/tests/gsm.test.js +181 -0
  66. package/dist/secrets/tests/mutation.test.d.ts +2 -0
  67. package/dist/secrets/tests/mutation.test.d.ts.map +1 -0
  68. package/dist/secrets/tests/mutation.test.js +390 -0
  69. package/dist/secrets/tests/secret-input.test.d.ts +2 -0
  70. package/dist/secrets/tests/secret-input.test.d.ts.map +1 -0
  71. package/dist/secrets/tests/secret-input.test.js +71 -0
  72. package/dist/secrets/tests/stream.test.d.ts +2 -0
  73. package/dist/secrets/tests/stream.test.d.ts.map +1 -0
  74. package/dist/secrets/tests/stream.test.js +88 -0
  75. package/dist/secrets/tests/temporary-secret.test.d.ts +2 -0
  76. package/dist/secrets/tests/temporary-secret.test.d.ts.map +1 -0
  77. package/dist/secrets/tests/temporary-secret.test.js +76 -0
  78. package/package.json +1 -1
  79. 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
- if (!this.env.BW_SESSION?.trim()) {
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 ids = values.flatMap((value) => {
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(ids).size !== ids.length) {
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 id of ids) {
97
- const itemResult = await this.runOrThrow(["get", "item", id], `Bitwarden item read failed for ${selector.prefix}`);
98
- const value = parseJson(itemResult, `Bitwarden item ${id}`);
99
- if (!isRecord(value)) {
100
- throw new Error(`Bitwarden returned an invalid item for ${selector.prefix}`);
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
- if (value.id !== id ||
107
- value.type !== 2 ||
108
- !Array.isArray(value.collectionIds) ||
109
- value.collectionIds.length !== 1 ||
110
- value.collectionIds[0] !== collection.id ||
111
- value.organizationId !== collection.organizationId) {
112
- throw new Error(`Bitwarden item ${typeof value.name === "string" ? value.name : id} is not a secure note in the selected collection`);
113
- }
114
- const attachment = parseAttachment(value.attachments, selector.prefix);
115
- if (attachment === undefined) {
116
- if (typeof value.notes !== "string") {
117
- throw new Error(`Bitwarden item ${value.name} is not a secure note in the selected collection`);
118
- }
119
- items.push({
120
- id,
121
- name: value.name,
122
- value: value.notes,
123
- selector,
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
- if (value.notes !== null &&
128
- (typeof value.notes !== "string" || value.notes.length > 0)) {
129
- throw new Error(`Bitwarden item notes must be null or empty when an attachment is present for ${selector.prefix}`);
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
- if (attachment.fileName !== attachmentFileName(value.name)) {
132
- throw new Error(`Bitwarden attachment filename does not match the item basename for ${selector.prefix}`);
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
- const attachmentResult = await this.runOrThrow(["get", "attachment", attachment.fileName, "--itemid", id, "--raw"], `Bitwarden attachment read failed for ${selector.prefix}`);
135
- items.push({
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 runOrThrow(args, message) {
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, { env: this.env });
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;
@@ -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 declare function parseCliArguments(argv: readonly string[]): ExecCliArguments;
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":";AASA,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;CAChB;AAUD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB,CAkD3E;AAED,wBAAsB,IAAI,CACxB,IAAI,GAAE,SAAS,MAAM,EAA0B,GAC9C,OAAO,CAAC,MAAM,CAAC,CAwBjB"}
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"}