@nightlybuildgroup/vault 1.1.2 → 1.2.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
@@ -30,6 +30,8 @@ password. Credentials are verified against the server before anything is saved.
30
30
  nbg-pw get "GitHub" # prints the password
31
31
  nbg-pw get "GitHub" --field username # a built-in field
32
32
  nbg-pw get "GitHub" --custom apiToken # a custom field, looked up by name
33
+ nbg-pw get "My Visa" --field number # a credit-card field (see below)
34
+ nbg-pw list "My Visa" # show an item's field names + types, no values
33
35
  nbg-pw serve --port 8087 # local bw REST API for fast repeated reads
34
36
  nbg-pw status # presence + auth state (no secrets)
35
37
  nbg-pw doctor # diagnose bw / Keychain / connectivity
@@ -37,6 +39,35 @@ nbg-pw logout # lock the session
37
39
  nbg-pw reset # log out and delete stored credentials
38
40
  ```
39
41
 
42
+ ### Credit-card items
43
+
44
+ Credit cards have no username or password — just card data. `bw` has no native
45
+ getter for those, so `nbg-pw` reads them from the item and exposes each as a
46
+ `--field`:
47
+
48
+ ```sh
49
+ nbg-pw get "My Visa" --field number # card number
50
+ nbg-pw get "My Visa" --field cvv # security code (alias: code)
51
+ nbg-pw get "My Visa" --field expiration # "MM/YYYY" (alias: exp)
52
+ nbg-pw get "My Visa" --field cardholder # cardholder name
53
+ nbg-pw get "My Visa" --field brand # e.g. Visa
54
+ ```
55
+
56
+ ### Discovering fields with `list`
57
+
58
+ Not sure what a custom field is called, or which fields an item has? `list`
59
+ prints the item type, the built-in fields you can `get`, and every custom field
60
+ name with its type — **never any values**:
61
+
62
+ ```sh
63
+ $ nbg-pw list "My Visa"
64
+ Type: card
65
+ Built-in: number, cvv, expiration, cardholder, brand
66
+ Custom fields:
67
+ billingZip (text)
68
+ pin (hidden)
69
+ ```
70
+
40
71
  ### Reading custom fields over `serve`
41
72
 
42
73
  `serve` is a thin wrapper around Bitwarden's own `bw serve` REST API, which has
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nightlybuildgroup/vault",
3
- "version": "1.1.2",
3
+ "version": "1.2.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
@@ -51,17 +51,57 @@ export async function getItem({ item, field, session }, deps = {}) {
51
51
  return stdout.replace(/\n$/, '');
52
52
  }
53
53
 
54
- export async function getCustomField({ item, name, session }, deps = {}) {
54
+ export async function getItemJson({ item, session }, deps = {}) {
55
55
  const run = deps.run ?? realRun;
56
56
  const { stdout } = await run('bw', ['get', 'item', item], {
57
57
  env: { BW_SESSION: session },
58
58
  });
59
- const parsed = JSON.parse(stdout);
59
+ return JSON.parse(stdout);
60
+ }
61
+
62
+ export async function getCustomField({ item, name, session }, deps = {}) {
63
+ const parsed = await getItemJson({ item, session }, deps);
60
64
  const match = (parsed.fields ?? []).find((f) => f.name === name);
61
65
  if (!match) throw new Error(`item "${item}" has no custom field "${name}"`);
62
66
  return match.value;
63
67
  }
64
68
 
69
+ // Credit-card items have no native `bw get <field>` getter, so card fields are
70
+ // read from the item's `card` object. Maps user-facing field names (and a few
71
+ // aliases) onto the bw card-object keys; `expiration` is computed, not a key.
72
+ const CARD_FIELD_ALIASES = {
73
+ number: 'number',
74
+ cvv: 'code',
75
+ code: 'code',
76
+ securitycode: 'code',
77
+ expiration: 'expiration',
78
+ exp: 'expiration',
79
+ cardholder: 'cardholderName',
80
+ cardholdername: 'cardholderName',
81
+ brand: 'brand',
82
+ };
83
+
84
+ export function isCardField(field) {
85
+ return Object.prototype.hasOwnProperty.call(CARD_FIELD_ALIASES, String(field).toLowerCase());
86
+ }
87
+
88
+ export async function getCardField({ item, field, session }, deps = {}) {
89
+ const parsed = await getItemJson({ item, session }, deps);
90
+ const card = parsed.card;
91
+ if (!card) throw new Error(`item "${item}" is not a credit card`);
92
+ const key = CARD_FIELD_ALIASES[String(field).toLowerCase()];
93
+ if (key === 'expiration') {
94
+ if (!card.expMonth && !card.expYear) throw new Error(`item "${item}" has no expiration date`);
95
+ const mm = String(card.expMonth ?? '').padStart(2, '0');
96
+ return `${mm}/${card.expYear ?? ''}`;
97
+ }
98
+ const value = card[key];
99
+ if (value === undefined || value === null || value === '') {
100
+ throw new Error(`item "${item}" has no ${field}`);
101
+ }
102
+ return String(value);
103
+ }
104
+
65
105
  export async function status(deps = {}) {
66
106
  const run = deps.run ?? realRun;
67
107
  const { stdout } = await run('bw', ['status']);
package/src/cli.js CHANGED
@@ -5,6 +5,7 @@ Usage: nbg-pw <command> [options]
5
5
  Commands:
6
6
  setup Interactive wizard: store your Vaultwarden credentials in the Keychain
7
7
  get Fetch a secret value (e.g. nbg-pw get "GitHub" --field password)
8
+ list Show an item's field names + types, no values (nbg-pw list "My Visa")
8
9
  serve Start a local bw API daemon (unlocked) for fast repeated reads
9
10
  status Show config + auth state (no secret values)
10
11
  doctor Diagnose bw / Keychain / server connectivity (--fix resets a wedged bw session)
@@ -45,9 +46,10 @@ export async function runCli(argv, deps = {}) {
45
46
  }
46
47
 
47
48
  async function loadCommands() {
48
- const [setup, get, status, doctor, serve, reset] = await Promise.all([
49
+ const [setup, get, list, status, doctor, serve, reset] = await Promise.all([
49
50
  import('./commands/setup.js'),
50
51
  import('./commands/get.js'),
52
+ import('./commands/list.js'),
51
53
  import('./commands/status.js'),
52
54
  import('./commands/doctor.js'),
53
55
  import('./commands/serve.js'),
@@ -56,6 +58,7 @@ async function loadCommands() {
56
58
  return {
57
59
  setup: setup.default,
58
60
  get: get.default,
61
+ list: list.default,
59
62
  status: status.default,
60
63
  doctor: doctor.default,
61
64
  serve: serve.default,
@@ -20,7 +20,7 @@ export function parseGetArgs(args) {
20
20
  }
21
21
  else if (!item) { item = args[i]; }
22
22
  }
23
- if (!item) throw new Error('usage: nbg-pw get <item> [--field password|username|uri|notes] [--custom <name>]');
23
+ if (!item) throw new Error('usage: nbg-pw get <item> [--field password|username|uri|notes|number|cvv|expiration|cardholder|brand] [--custom <name>]');
24
24
  if (fieldGiven && custom !== null) throw new Error('--field and --custom are mutually exclusive');
25
25
  const result = { item, field };
26
26
  if (custom !== null) result.custom = custom;
@@ -38,9 +38,14 @@ export async function runGet(args, deps) {
38
38
  }
39
39
 
40
40
  const session = await bw.ensureSession(config);
41
- const value = custom !== undefined
42
- ? await bw.getCustomField({ item, name: custom, session })
43
- : await bw.getItem({ item, field, session });
41
+ let value;
42
+ if (custom !== undefined) {
43
+ value = await bw.getCustomField({ item, name: custom, session });
44
+ } else if (bw.isCardField?.(field)) {
45
+ value = await bw.getCardField({ item, field, session });
46
+ } else {
47
+ value = await bw.getItem({ item, field, session });
48
+ }
44
49
  out(value + '\n');
45
50
  return 0;
46
51
  }
@@ -0,0 +1,85 @@
1
+ import * as bwModule from '../bw.js';
2
+ import * as keychainModule from '../keychain.js';
3
+
4
+ const ITEM_TYPES = { 1: 'login', 2: 'note', 3: 'card', 4: 'identity' };
5
+ const FIELD_TYPE_LABELS = { 0: 'text', 1: 'hidden', 2: 'boolean', 3: 'linked' };
6
+
7
+ export function parseListArgs(args) {
8
+ let item = null;
9
+ for (const a of args) {
10
+ if (!item) item = a;
11
+ }
12
+ if (!item) throw new Error('usage: nbg-pw list <item>');
13
+ return { item };
14
+ }
15
+
16
+ // The built-in (non-custom) fields you can `nbg-pw get` for this item — only the
17
+ // ones actually populated, so the list mirrors what a `get` would return.
18
+ function builtinFields(parsed) {
19
+ if (parsed.type === 3 && parsed.card) {
20
+ const c = parsed.card;
21
+ return [
22
+ c.number && 'number',
23
+ c.code && 'cvv',
24
+ (c.expMonth || c.expYear) && 'expiration',
25
+ c.cardholderName && 'cardholder',
26
+ c.brand && 'brand',
27
+ ].filter(Boolean);
28
+ }
29
+ if (parsed.type === 1 && parsed.login) {
30
+ const l = parsed.login;
31
+ return [
32
+ l.username && 'username',
33
+ l.password && 'password',
34
+ l.uris && l.uris.length && 'uri',
35
+ l.totp && 'totp',
36
+ parsed.notes && 'notes',
37
+ ].filter(Boolean);
38
+ }
39
+ return parsed.notes ? ['notes'] : [];
40
+ }
41
+
42
+ export function formatItem(parsed) {
43
+ const lines = [`Type: ${ITEM_TYPES[parsed.type] ?? `unknown (${parsed.type})`}`];
44
+
45
+ const builtin = builtinFields(parsed);
46
+ if (builtin.length) lines.push(`Built-in: ${builtin.join(', ')}`);
47
+
48
+ const fields = parsed.fields ?? [];
49
+ if (fields.length === 0) {
50
+ lines.push('Custom fields: (none)');
51
+ } else {
52
+ lines.push('Custom fields:');
53
+ const width = Math.max(...fields.map((f) => f.name.length));
54
+ for (const f of fields) {
55
+ const label = FIELD_TYPE_LABELS[f.type] ?? `type ${f.type}`;
56
+ lines.push(` ${f.name.padEnd(width)} (${label})`);
57
+ }
58
+ }
59
+ return lines.join('\n') + '\n';
60
+ }
61
+
62
+ export async function runList(args, deps) {
63
+ const { keychain, bw, out, err } = deps;
64
+ const { item } = parseListArgs(args);
65
+
66
+ const config = await keychain.readConfig();
67
+ if (!config) {
68
+ err('No stored credentials. Run "nbg-pw setup" first.\n');
69
+ return 1;
70
+ }
71
+
72
+ const session = await bw.ensureSession(config);
73
+ const parsed = await bw.getItemJson({ item, session });
74
+ out(formatItem(parsed));
75
+ return 0;
76
+ }
77
+
78
+ export default async function list(args) {
79
+ return runList(args, {
80
+ keychain: keychainModule,
81
+ bw: bwModule,
82
+ out: (s) => process.stdout.write(s),
83
+ err: (s) => process.stderr.write(s),
84
+ });
85
+ }