@nightlybuildgroup/vault 1.4.0 → 1.6.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/README.md CHANGED
@@ -38,6 +38,7 @@ nbg-pw attach "GitHub" --file ./key.pem # add an attachment; --list/--get/--de
38
38
  nbg-pw delete "GitHub" # soft-delete to trash (alias: rm; --permanent skips it)
39
39
  nbg-pw items --search git # list item names; also: folders, collections
40
40
  nbg-pw generate --length 24 --symbols # generate a password (no vault needed)
41
+ nbg-pw config set collection "Shared" # new items share into this org collection
41
42
  nbg-pw serve --port 8087 # local bw REST API for fast repeated reads
42
43
  nbg-pw status # presence + auth state (no secrets)
43
44
  nbg-pw doctor # diagnose bw / Keychain / connectivity
@@ -110,6 +111,33 @@ for path in $(keepassxc-cli ls -R -f "$DB"); do
110
111
  done
111
112
  ```
112
113
 
114
+ ### Sharing new items with an organization (`config`)
115
+
116
+ By default `add` creates items in the API account's **personal** vault, which
117
+ other org members (and your own interactive login) can't see. To make every new
118
+ item land in a shared **organization collection** instead, set a default — it's
119
+ stored in your Keychain, never in this repo, so nothing org-specific is baked
120
+ into the tool:
121
+
122
+ ```sh
123
+ nbg-pw config set organization "Acme Inc" # by name or id
124
+ nbg-pw config set collection "Shared" # by name or id
125
+ nbg-pw config show # current defaults
126
+ nbg-pw config unset collection # back to personal-vault default
127
+ ```
128
+
129
+ Once a default collection is set, every `add` shares the new item into it:
130
+
131
+ ```sh
132
+ printf '%s' "$pw" | nbg-pw add "GitHub" --username octocat --password-stdin
133
+ # → Created "GitHub" (…) → shared to "Shared"
134
+
135
+ nbg-pw add "Personal Note" --personal # opt a single item out (personal vault)
136
+ ```
137
+
138
+ An explicit `--collection`/`--organization` on `add` overrides the default for
139
+ that call. `status` shows the active default share.
140
+
113
141
  ### Editing items (`edit`)
114
142
 
115
143
  Fetch-merge-write: only the flags you pass are changed; everything else stays.
@@ -153,8 +181,22 @@ nbg-pw items --search git # filtered by search
153
181
  nbg-pw items --folder "Agents" # filtered by folder
154
182
  nbg-pw folders # folder names
155
183
  nbg-pw collections # collection names
184
+ nbg-pw organizations # organization names
185
+ ```
186
+
187
+ Add `--ids` to any of them to print ids alongside names — useful for `config set`
188
+ or scripting. `collections --ids` also shows each collection's org id:
189
+
190
+ ```sh
191
+ $ nbg-pw organizations --ids
192
+ dfff… Acme Inc
193
+ $ nbg-pw collections --ids
194
+ bf2b… Shared (org dfff…)
156
195
  ```
157
196
 
197
+ (You can pass either a **name or an id** to `config set` / `add --collection`, so
198
+ ids are optional — handy mainly when names are ambiguous.)
199
+
158
200
  ### Generating secrets (`generate`)
159
201
 
160
202
  Pure generator — needs no vault session.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nightlybuildgroup/vault",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Store Vaultwarden/Bitwarden credentials in the macOS Keychain and read secrets back for local agents.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/bw.js CHANGED
@@ -144,14 +144,29 @@ export async function listOrganizations({ session }, deps = {}) {
144
144
  return JSON.parse(stdout);
145
145
  }
146
146
 
147
+ export async function sync({ session }, deps = {}) {
148
+ const run = deps.run ?? realRun;
149
+ await run('bw', ['sync'], { env: { BW_SESSION: session } });
150
+ }
151
+
147
152
  // Share an existing item into an organization's collection(s). This is bw's only
148
- // path to put an item in a collection; the item must already exist.
153
+ // path to put an item in a collection; the item must already exist. bw's local
154
+ // cache can lag a just-created item ("client copy ... out of date"); a sync +
155
+ // single retry clears that.
149
156
  export async function moveItemToCollection({ itemId, organizationId, collectionIds, session }, deps = {}) {
150
157
  const run = deps.run ?? realRun;
151
- await run('bw', ['move', itemId, organizationId], {
152
- env: { BW_SESSION: session },
153
- input: encode(collectionIds),
154
- });
158
+ const move = () =>
159
+ run('bw', ['move', itemId, organizationId], { env: { BW_SESSION: session }, input: encode(collectionIds) });
160
+ try {
161
+ await move();
162
+ } catch (e) {
163
+ if (/out of date/i.test(e.message || '')) {
164
+ await run('bw', ['sync'], { env: { BW_SESSION: session } });
165
+ await move();
166
+ } else {
167
+ throw e;
168
+ }
169
+ }
155
170
  }
156
171
 
157
172
  export async function editItem({ id, item, session }, deps = {}) {
package/src/cli.js CHANGED
@@ -10,10 +10,12 @@ Commands:
10
10
  delete Delete an item or folder (alias: rm; --permanent skips trash)
11
11
  attach Manage an item's attachments (--file/--list/--get/--delete)
12
12
  list Show an item's field names + types, no values (nbg-pw list "My Visa")
13
- items List item names (--search, --folder, --collection)
14
- folders List folder names
15
- collections List collection names
13
+ items List item names (--search, --folder, --collection; --ids)
14
+ folders List folder names (--ids)
15
+ collections List collection names (--ids shows collection + org ids)
16
+ organizations List organization names (--ids)
16
17
  generate Generate a password or passphrase (bw generate)
18
+ config Show/set defaults, e.g. the org collection new items share into
17
19
  serve Start a local bw API daemon (unlocked) for fast repeated reads
18
20
  status Show config + auth state (no secret values)
19
21
  doctor Diagnose bw / Keychain / server connectivity (--fix resets a wedged bw session)
@@ -54,7 +56,7 @@ export async function runCli(argv, deps = {}) {
54
56
  }
55
57
 
56
58
  async function loadCommands() {
57
- const [setup, get, add, edit, del, attach, list, items, folders, collections, generate, status, doctor, serve, reset] =
59
+ const [setup, get, add, edit, del, attach, list, items, folders, collections, organizations, generate, config, status, doctor, serve, reset] =
58
60
  await Promise.all([
59
61
  import('./commands/setup.js'),
60
62
  import('./commands/get.js'),
@@ -66,7 +68,9 @@ async function loadCommands() {
66
68
  import('./commands/items.js'),
67
69
  import('./commands/folders.js'),
68
70
  import('./commands/collections.js'),
71
+ import('./commands/organizations.js'),
69
72
  import('./commands/generate.js'),
73
+ import('./commands/config.js'),
70
74
  import('./commands/status.js'),
71
75
  import('./commands/doctor.js'),
72
76
  import('./commands/serve.js'),
@@ -84,7 +88,9 @@ async function loadCommands() {
84
88
  items: items.default,
85
89
  folders: folders.default,
86
90
  collections: collections.default,
91
+ organizations: organizations.default,
87
92
  generate: generate.default,
93
+ config: config.default,
88
94
  status: status.default,
89
95
  doctor: doctor.default,
90
96
  serve: serve.default,
@@ -5,7 +5,7 @@ const USAGE =
5
5
  'usage: nbg-pw add <name> [--username <u>] [--url <uri>]... [--notes <n>] ' +
6
6
  '[--folder <name>] [--collection <name>] [--organization <name|id>] ' +
7
7
  '[--field <name>=<value>]... [--field-hidden <name>=<value>]... [--totp <secret>] ' +
8
- '[--password-stdin]\n or: nbg-pw add --json (reads one entry as a JSON object on stdin)';
8
+ '[--password-stdin] [--personal]\n or: nbg-pw add --json (reads one entry as a JSON object on stdin)';
9
9
 
10
10
  // `--field name=value` → { name, value, type }. Splits on the FIRST `=` so the
11
11
  // value may itself contain `=`.
@@ -16,7 +16,7 @@ function parseField(raw, type) {
16
16
  }
17
17
 
18
18
  export function parseAddArgs(args) {
19
- const opts = { uris: [], fields: [], passwordStdin: false, json: false };
19
+ const opts = { uris: [], fields: [], passwordStdin: false, json: false, personal: false };
20
20
  let name = null;
21
21
  for (let i = 0; i < args.length; i++) {
22
22
  const a = args[i];
@@ -27,6 +27,7 @@ export function parseAddArgs(args) {
27
27
  };
28
28
  switch (a) {
29
29
  case '--json': opts.json = true; break;
30
+ case '--personal': opts.personal = true; break;
30
31
  case '--password-stdin': opts.passwordStdin = true; break;
31
32
  case '--username': opts.username = need(); break;
32
33
  case '--url': opts.uris.push(need()); break;
@@ -122,23 +123,36 @@ export async function runAdd(args, deps) {
122
123
  if (opts.passwordStdin) entry.password = (await readStdin()).replace(/\r?\n$/, '');
123
124
  }
124
125
 
126
+ // Where the item lands: an explicit --collection wins; otherwise the
127
+ // configured default shares every new item into the org collection, unless
128
+ // --personal (or a json `personal:true`) opts out into the personal vault.
129
+ const personal = opts.personal || entry.personal;
130
+ let collectionName = entry.collection;
131
+ let organizationName = entry.organization;
132
+ if (!personal && !collectionName && config.defaultCollection) {
133
+ collectionName = config.defaultCollection;
134
+ organizationName = organizationName ?? config.defaultOrganization;
135
+ }
136
+
125
137
  const session = await bw.ensureSession(config);
126
138
 
127
139
  if (entry.folder) entry.folderId = await resolveFolderId(bw, session, entry.folder);
128
140
 
129
141
  const created = await bw.createItem({ item: buildLoginItem(entry), session });
130
142
 
131
- if (entry.collection) {
132
- const col = await resolveCollection(bw, session, entry.collection, entry.organization);
143
+ let shared = '';
144
+ if (collectionName) {
145
+ const col = await resolveCollection(bw, session, collectionName, organizationName);
133
146
  await bw.moveItemToCollection({
134
147
  itemId: created.id,
135
148
  organizationId: col.organizationId,
136
149
  collectionIds: [col.id],
137
150
  session,
138
151
  });
152
+ shared = ` → shared to "${col.name}"`;
139
153
  }
140
154
 
141
- out(`Created "${created.name}" (${created.id})\n`);
155
+ out(`Created "${created.name}" (${created.id})${shared}\n`);
142
156
  return 0;
143
157
  }
144
158
 
@@ -2,13 +2,17 @@ import * as bwModule from '../bw.js';
2
2
  import * as keychainModule from '../keychain.js';
3
3
 
4
4
  export function parseCollectionsArgs(args) {
5
- if (args.length > 0) throw new Error(`unexpected argument: ${args[0]}`);
6
- return {};
5
+ let ids = false;
6
+ for (const a of args) {
7
+ if (a === '--ids') ids = true;
8
+ else throw new Error(`unexpected argument: ${a}`);
9
+ }
10
+ return { ids };
7
11
  }
8
12
 
9
13
  export async function runCollections(args, deps) {
10
14
  const { keychain, bw, out, err } = deps;
11
- parseCollectionsArgs(args);
15
+ const { ids } = parseCollectionsArgs(args);
12
16
 
13
17
  const config = await keychain.readConfig();
14
18
  if (!config) {
@@ -17,11 +21,11 @@ export async function runCollections(args, deps) {
17
21
  }
18
22
 
19
23
  const session = await bw.ensureSession(config);
20
- const collections = await bw.listCollections({ session });
21
- const names = collections
22
- .map((c) => c.name)
23
- .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
24
- for (const name of names) out(name + '\n');
24
+ const collections = (await bw.listCollections({ session }))
25
+ .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
26
+ for (const c of collections) {
27
+ out(ids ? `${c.id} ${c.name} (org ${c.organizationId})\n` : c.name + '\n');
28
+ }
25
29
  return 0;
26
30
  }
27
31
 
@@ -0,0 +1,60 @@
1
+ import * as keychainModule from '../keychain.js';
2
+
3
+ // User-facing key → stored config field.
4
+ const KEYS = { organization: 'defaultOrganization', collection: 'defaultCollection' };
5
+
6
+ const USAGE = 'usage: nbg-pw config [show | set <organization|collection> <value> | unset <organization|collection>]';
7
+
8
+ export function parseConfigArgs(args) {
9
+ const [sub, ...rest] = args;
10
+ if (!sub || sub === 'show') return { action: 'show' };
11
+ if (sub === 'set') {
12
+ const [key, value] = rest;
13
+ if (!key || !(key in KEYS)) throw new Error(USAGE);
14
+ if (value === undefined) throw new Error('config set requires a value');
15
+ return { action: 'set', key, value };
16
+ }
17
+ if (sub === 'unset') {
18
+ const [key] = rest;
19
+ if (!key || !(key in KEYS)) throw new Error(USAGE);
20
+ return { action: 'unset', key };
21
+ }
22
+ throw new Error(`unknown config subcommand "${sub}"\n${USAGE}`);
23
+ }
24
+
25
+ export async function runConfig(args, deps) {
26
+ const { keychain, out, err } = deps;
27
+ const opts = parseConfigArgs(args);
28
+
29
+ const config = await keychain.readConfig();
30
+ if (!config) {
31
+ err('No stored credentials. Run "nbg-pw setup" first.\n');
32
+ return 1;
33
+ }
34
+
35
+ if (opts.action === 'show') {
36
+ out(`default organization: ${config.defaultOrganization ?? '(not set)'}\n`);
37
+ out(`default collection: ${config.defaultCollection ?? '(not set)'}\n`);
38
+ return 0;
39
+ }
40
+
41
+ const field = KEYS[opts.key];
42
+ if (opts.action === 'set') {
43
+ config[field] = opts.value;
44
+ await keychain.writeConfig(config);
45
+ out(`Set default ${opts.key} = ${opts.value}\n`);
46
+ } else {
47
+ delete config[field];
48
+ await keychain.writeConfig(config);
49
+ out(`Unset default ${opts.key}\n`);
50
+ }
51
+ return 0;
52
+ }
53
+
54
+ export default async function config(args) {
55
+ return runConfig(args, {
56
+ keychain: keychainModule,
57
+ out: (s) => process.stdout.write(s),
58
+ err: (s) => process.stderr.write(s),
59
+ });
60
+ }
@@ -2,13 +2,17 @@ import * as bwModule from '../bw.js';
2
2
  import * as keychainModule from '../keychain.js';
3
3
 
4
4
  export function parseFoldersArgs(args) {
5
- if (args.length > 0) throw new Error(`unexpected argument: ${args[0]}`);
6
- return {};
5
+ let ids = false;
6
+ for (const a of args) {
7
+ if (a === '--ids') ids = true;
8
+ else throw new Error(`unexpected argument: ${a}`);
9
+ }
10
+ return { ids };
7
11
  }
8
12
 
9
13
  export async function runFolders(args, deps) {
10
14
  const { keychain, bw, out, err } = deps;
11
- parseFoldersArgs(args);
15
+ const { ids } = parseFoldersArgs(args);
12
16
 
13
17
  const config = await keychain.readConfig();
14
18
  if (!config) {
@@ -17,11 +21,11 @@ export async function runFolders(args, deps) {
17
21
  }
18
22
 
19
23
  const session = await bw.ensureSession(config);
20
- const folders = await bw.listFolders({ session });
21
- const names = folders
22
- .map((f) => f.name)
23
- .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
24
- for (const name of names) out(name + '\n');
24
+ const folders = (await bw.listFolders({ session }))
25
+ .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
26
+ for (const f of folders) {
27
+ out(ids ? `${f.id ?? '-'} ${f.name}\n` : f.name + '\n');
28
+ }
25
29
  return 0;
26
30
  }
27
31
 
@@ -5,6 +5,7 @@ export function parseItemsArgs(args) {
5
5
  let search = null;
6
6
  let folder = null;
7
7
  let collection = null;
8
+ let ids = false;
8
9
  for (let i = 0; i < args.length; i++) {
9
10
  if (args[i] === '--search') {
10
11
  const v = args[++i];
@@ -18,16 +19,18 @@ export function parseItemsArgs(args) {
18
19
  const v = args[++i];
19
20
  if (v === undefined) throw new Error('--collection requires a value');
20
21
  collection = v;
22
+ } else if (args[i] === '--ids') {
23
+ ids = true;
21
24
  } else {
22
25
  throw new Error(`unknown option: ${args[i]}`);
23
26
  }
24
27
  }
25
- return { search, folder, collection };
28
+ return { search, folder, collection, ids };
26
29
  }
27
30
 
28
31
  export async function runItems(args, deps) {
29
32
  const { keychain, bw, out, err } = deps;
30
- const { search, folder, collection } = parseItemsArgs(args);
33
+ const { search, folder, collection, ids } = parseItemsArgs(args);
31
34
 
32
35
  const config = await keychain.readConfig();
33
36
  if (!config) {
@@ -58,11 +61,11 @@ export async function runItems(args, deps) {
58
61
  if (folderId !== undefined) filters.folderId = folderId;
59
62
  if (collectionId !== undefined) filters.collectionId = collectionId;
60
63
 
61
- const items = await bw.listItems(filters);
62
- const names = items
63
- .map((i) => i.name)
64
- .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
65
- for (const name of names) out(name + '\n');
64
+ const items = (await bw.listItems(filters))
65
+ .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
66
+ for (const i of items) {
67
+ out(ids ? `${i.id} ${i.name}\n` : i.name + '\n');
68
+ }
66
69
  return 0;
67
70
  }
68
71
 
@@ -0,0 +1,39 @@
1
+ import * as bwModule from '../bw.js';
2
+ import * as keychainModule from '../keychain.js';
3
+
4
+ export function parseOrganizationsArgs(args) {
5
+ let ids = false;
6
+ for (const a of args) {
7
+ if (a === '--ids') ids = true;
8
+ else throw new Error(`unexpected argument: ${a}`);
9
+ }
10
+ return { ids };
11
+ }
12
+
13
+ export async function runOrganizations(args, deps) {
14
+ const { keychain, bw, out, err } = deps;
15
+ const { ids } = parseOrganizationsArgs(args);
16
+
17
+ const config = await keychain.readConfig();
18
+ if (!config) {
19
+ err('No stored credentials. Run "nbg-pw setup" first.\n');
20
+ return 1;
21
+ }
22
+
23
+ const session = await bw.ensureSession(config);
24
+ const orgs = (await bw.listOrganizations({ session }))
25
+ .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
26
+ for (const o of orgs) {
27
+ out(ids ? `${o.id} ${o.name}\n` : o.name + '\n');
28
+ }
29
+ return 0;
30
+ }
31
+
32
+ export default async function organizations(args) {
33
+ return runOrganizations(args, {
34
+ keychain: keychainModule,
35
+ bw: bwModule,
36
+ out: (s) => process.stdout.write(s),
37
+ err: (s) => process.stderr.write(s),
38
+ });
39
+ }
@@ -12,6 +12,10 @@ export async function runStatus(args, deps) {
12
12
  out(` stored creds: yes\n`);
13
13
  out(` server URL: ${config.serverUrl}\n`);
14
14
  out(` email: ${config.email}\n`);
15
+ if (config.defaultCollection) {
16
+ const org = config.defaultOrganization ? `${config.defaultOrganization} / ` : '';
17
+ out(` default share: ${org}${config.defaultCollection}\n`);
18
+ }
15
19
  } else {
16
20
  out(' stored creds: No credentials stored (run "nbg-pw setup")\n');
17
21
  }
package/src/config.js CHANGED
@@ -2,6 +2,8 @@ export const SERVICE = 'com.nightlybuild.vault';
2
2
  export const ACCOUNT = 'default';
3
3
  export const FIELDS = ['serverUrl', 'email', 'clientId', 'clientSecret', 'masterPassword'];
4
4
  export const READ_FIELDS = [...FIELDS, 'savedAt'];
5
+ // Optional: when set, `add` shares new items into this org collection by default.
6
+ export const OPTIONAL_FIELDS = ['defaultOrganization', 'defaultCollection'];
5
7
 
6
8
  export function isValidUrl(s) {
7
9
  if (typeof s !== 'string' || s.length === 0) return false;
@@ -26,6 +28,11 @@ export function validateConfig(obj) {
26
28
  }
27
29
  if (!isValidUrl(obj.serverUrl)) throw new Error('config serverUrl is not a valid URL');
28
30
  if (!isValidEmail(obj.email)) throw new Error('config email is not a valid email');
31
+ for (const f of OPTIONAL_FIELDS) {
32
+ if (obj[f] !== undefined && (typeof obj[f] !== 'string' || obj[f].length === 0)) {
33
+ throw new Error(`config field ${f} must be a non-empty string when set`);
34
+ }
35
+ }
29
36
  return obj;
30
37
  }
31
38