@nightlybuildgroup/vault 1.1.2 → 1.3.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 +97 -0
- package/package.json +1 -1
- package/src/bw.js +94 -2
- package/src/cli.js +7 -1
- package/src/commands/add.js +163 -0
- package/src/commands/get.js +9 -4
- package/src/commands/list.js +85 -0
- package/src/exec.js +6 -0
package/README.md
CHANGED
|
@@ -30,6 +30,9 @@ 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
|
|
35
|
+
printf '%s' "$pw" | nbg-pw add "GitHub" --username octocat --password-stdin
|
|
33
36
|
nbg-pw serve --port 8087 # local bw REST API for fast repeated reads
|
|
34
37
|
nbg-pw status # presence + auth state (no secrets)
|
|
35
38
|
nbg-pw doctor # diagnose bw / Keychain / connectivity
|
|
@@ -37,6 +40,100 @@ nbg-pw logout # lock the session
|
|
|
37
40
|
nbg-pw reset # log out and delete stored credentials
|
|
38
41
|
```
|
|
39
42
|
|
|
43
|
+
### Adding items (`add`)
|
|
44
|
+
|
|
45
|
+
Creates a **login** item. The password never goes on the command line (it would
|
|
46
|
+
be visible in `ps`) — it's read from stdin, or supplied via `--json`.
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
# Flag mode — password piped on stdin:
|
|
50
|
+
printf '%s' "$pw" | nbg-pw add "GitHub" \
|
|
51
|
+
--username octocat \
|
|
52
|
+
--url https://github.com \
|
|
53
|
+
--notes "work account" \
|
|
54
|
+
--folder "Agents" \
|
|
55
|
+
--field plan=pro \
|
|
56
|
+
--field-hidden token="$tok" \
|
|
57
|
+
--password-stdin
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Options: `--username`, `--url` (repeatable), `--notes`, `--folder` (resolved by
|
|
61
|
+
name, **created if missing**), `--collection` / `--organization` (shares the item
|
|
62
|
+
into an org collection), `--field name=value` (text, repeatable),
|
|
63
|
+
`--field-hidden name=value` (hidden, repeatable), `--totp`. On success it prints
|
|
64
|
+
`Created "<name>" (<id>)` — never any secret value.
|
|
65
|
+
|
|
66
|
+
> Note: in flag mode, `--field-hidden` values still pass through argv. For
|
|
67
|
+
> secret custom fields use `--json` (below), which keeps **everything** off the
|
|
68
|
+
> command line.
|
|
69
|
+
|
|
70
|
+
#### JSON mode — secret-safe bulk automation
|
|
71
|
+
|
|
72
|
+
`nbg-pw add --json` reads one entry as a JSON object on stdin, so every value
|
|
73
|
+
(password, hidden fields, notes) stays off argv:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
jq -n --arg name GitHub --arg u octocat --arg p "$pw" --arg t "$tok" \
|
|
77
|
+
'{name:$name, username:$u, password:$p, uris:["https://github.com"],
|
|
78
|
+
folder:"Agents", fields:[{name:"token", value:$t, type:1}]}' \
|
|
79
|
+
| nbg-pw add --json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Accepted keys: `name` (required), `username`, `password`, `uris` (array),
|
|
83
|
+
`notes`, `totp`, `folder`, `collection`, `organization`, and `fields`
|
|
84
|
+
(`[{name, value, type}]`; type `0` = text, `1` = hidden).
|
|
85
|
+
|
|
86
|
+
#### Migrating a KeePass `.kdbx` to Bitwarden
|
|
87
|
+
|
|
88
|
+
Loop your KeePass entries and pipe each into `add --json` — secrets flow through
|
|
89
|
+
stdin only:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
DB=~/Documents/Agents.kdbx
|
|
93
|
+
for path in $(keepassxc-cli ls -R -f "$DB"); do
|
|
94
|
+
[ "${path%/}" != "$path" ] && continue # skip groups
|
|
95
|
+
user=$(keepassxc-cli show -q -a UserName "$DB" "$path")
|
|
96
|
+
pass=$(keepassxc-cli show -q -a Password "$DB" "$path")
|
|
97
|
+
url=$( keepassxc-cli show -q -a URL "$DB" "$path")
|
|
98
|
+
group=$(dirname "$path")
|
|
99
|
+
jq -n --arg n "$(basename "$path")" --arg u "$user" --arg p "$pass" \
|
|
100
|
+
--arg url "$url" --arg f "$group" \
|
|
101
|
+
'{name:$n, username:$u, password:$p,
|
|
102
|
+
uris:(if $url=="" then [] else [$url] end),
|
|
103
|
+
folder:(if $f=="." then null else $f end)}' \
|
|
104
|
+
| nbg-pw add --json
|
|
105
|
+
done
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Credit-card items
|
|
109
|
+
|
|
110
|
+
Credit cards have no username or password — just card data. `bw` has no native
|
|
111
|
+
getter for those, so `nbg-pw` reads them from the item and exposes each as a
|
|
112
|
+
`--field`:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
nbg-pw get "My Visa" --field number # card number
|
|
116
|
+
nbg-pw get "My Visa" --field cvv # security code (alias: code)
|
|
117
|
+
nbg-pw get "My Visa" --field expiration # "MM/YYYY" (alias: exp)
|
|
118
|
+
nbg-pw get "My Visa" --field cardholder # cardholder name
|
|
119
|
+
nbg-pw get "My Visa" --field brand # e.g. Visa
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Discovering fields with `list`
|
|
123
|
+
|
|
124
|
+
Not sure what a custom field is called, or which fields an item has? `list`
|
|
125
|
+
prints the item type, the built-in fields you can `get`, and every custom field
|
|
126
|
+
name with its type — **never any values**:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
$ nbg-pw list "My Visa"
|
|
130
|
+
Type: card
|
|
131
|
+
Built-in: number, cvv, expiration, cardholder, brand
|
|
132
|
+
Custom fields:
|
|
133
|
+
billingZip (text)
|
|
134
|
+
pin (hidden)
|
|
135
|
+
```
|
|
136
|
+
|
|
40
137
|
### Reading custom fields over `serve`
|
|
41
138
|
|
|
42
139
|
`serve` is a thin wrapper around Bitwarden's own `bw serve` REST API, which has
|
package/package.json
CHANGED
package/src/bw.js
CHANGED
|
@@ -51,17 +51,109 @@ export async function getItem({ item, field, session }, deps = {}) {
|
|
|
51
51
|
return stdout.replace(/\n$/, '');
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
export async function
|
|
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
|
-
|
|
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
|
+
|
|
105
|
+
// `bw create`/`bw move` read the object as base64 JSON. We pass it on stdin
|
|
106
|
+
// (never argv), so secret values in the payload stay off the process list.
|
|
107
|
+
function encode(obj) {
|
|
108
|
+
return Buffer.from(JSON.stringify(obj)).toString('base64');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function createItem({ item, session }, deps = {}) {
|
|
112
|
+
const run = deps.run ?? realRun;
|
|
113
|
+
const { stdout } = await run('bw', ['create', 'item'], {
|
|
114
|
+
env: { BW_SESSION: session },
|
|
115
|
+
input: encode(item),
|
|
116
|
+
});
|
|
117
|
+
return JSON.parse(stdout);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function listFolders({ session }, deps = {}) {
|
|
121
|
+
const run = deps.run ?? realRun;
|
|
122
|
+
const { stdout } = await run('bw', ['list', 'folders'], { env: { BW_SESSION: session } });
|
|
123
|
+
return JSON.parse(stdout);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function createFolder({ name, session }, deps = {}) {
|
|
127
|
+
const run = deps.run ?? realRun;
|
|
128
|
+
const { stdout } = await run('bw', ['create', 'folder'], {
|
|
129
|
+
env: { BW_SESSION: session },
|
|
130
|
+
input: encode({ name }),
|
|
131
|
+
});
|
|
132
|
+
return JSON.parse(stdout);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function listCollections({ session }, deps = {}) {
|
|
136
|
+
const run = deps.run ?? realRun;
|
|
137
|
+
const { stdout } = await run('bw', ['list', 'collections'], { env: { BW_SESSION: session } });
|
|
138
|
+
return JSON.parse(stdout);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function listOrganizations({ session }, deps = {}) {
|
|
142
|
+
const run = deps.run ?? realRun;
|
|
143
|
+
const { stdout } = await run('bw', ['list', 'organizations'], { env: { BW_SESSION: session } });
|
|
144
|
+
return JSON.parse(stdout);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 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.
|
|
149
|
+
export async function moveItemToCollection({ itemId, organizationId, collectionIds, session }, deps = {}) {
|
|
150
|
+
const run = deps.run ?? realRun;
|
|
151
|
+
await run('bw', ['move', itemId, organizationId], {
|
|
152
|
+
env: { BW_SESSION: session },
|
|
153
|
+
input: encode(collectionIds),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
65
157
|
export async function status(deps = {}) {
|
|
66
158
|
const run = deps.run ?? realRun;
|
|
67
159
|
const { stdout } = await run('bw', ['status']);
|
package/src/cli.js
CHANGED
|
@@ -5,6 +5,8 @@ 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
|
+
add Create a login item (flags or --json on stdin; for kdbx → bw imports)
|
|
9
|
+
list Show an item's field names + types, no values (nbg-pw list "My Visa")
|
|
8
10
|
serve Start a local bw API daemon (unlocked) for fast repeated reads
|
|
9
11
|
status Show config + auth state (no secret values)
|
|
10
12
|
doctor Diagnose bw / Keychain / server connectivity (--fix resets a wedged bw session)
|
|
@@ -45,9 +47,11 @@ export async function runCli(argv, deps = {}) {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
async function loadCommands() {
|
|
48
|
-
const [setup, get, status, doctor, serve, reset] = await Promise.all([
|
|
50
|
+
const [setup, get, add, list, status, doctor, serve, reset] = await Promise.all([
|
|
49
51
|
import('./commands/setup.js'),
|
|
50
52
|
import('./commands/get.js'),
|
|
53
|
+
import('./commands/add.js'),
|
|
54
|
+
import('./commands/list.js'),
|
|
51
55
|
import('./commands/status.js'),
|
|
52
56
|
import('./commands/doctor.js'),
|
|
53
57
|
import('./commands/serve.js'),
|
|
@@ -56,6 +60,8 @@ async function loadCommands() {
|
|
|
56
60
|
return {
|
|
57
61
|
setup: setup.default,
|
|
58
62
|
get: get.default,
|
|
63
|
+
add: add.default,
|
|
64
|
+
list: list.default,
|
|
59
65
|
status: status.default,
|
|
60
66
|
doctor: doctor.default,
|
|
61
67
|
serve: serve.default,
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as bwModule from '../bw.js';
|
|
2
|
+
import * as keychainModule from '../keychain.js';
|
|
3
|
+
|
|
4
|
+
const USAGE =
|
|
5
|
+
'usage: nbg-pw add <name> [--username <u>] [--url <uri>]... [--notes <n>] ' +
|
|
6
|
+
'[--folder <name>] [--collection <name>] [--organization <name|id>] ' +
|
|
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)';
|
|
9
|
+
|
|
10
|
+
// `--field name=value` → { name, value, type }. Splits on the FIRST `=` so the
|
|
11
|
+
// value may itself contain `=`.
|
|
12
|
+
function parseField(raw, type) {
|
|
13
|
+
const eq = raw.indexOf('=');
|
|
14
|
+
if (eq === -1) throw new Error(`--field expects name=value, got "${raw}"`);
|
|
15
|
+
return { name: raw.slice(0, eq), value: raw.slice(eq + 1), type };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseAddArgs(args) {
|
|
19
|
+
const opts = { uris: [], fields: [], passwordStdin: false, json: false };
|
|
20
|
+
let name = null;
|
|
21
|
+
for (let i = 0; i < args.length; i++) {
|
|
22
|
+
const a = args[i];
|
|
23
|
+
const need = () => {
|
|
24
|
+
const v = args[++i];
|
|
25
|
+
if (v === undefined) throw new Error(`${a} requires a value`);
|
|
26
|
+
return v;
|
|
27
|
+
};
|
|
28
|
+
switch (a) {
|
|
29
|
+
case '--json': opts.json = true; break;
|
|
30
|
+
case '--password-stdin': opts.passwordStdin = true; break;
|
|
31
|
+
case '--username': opts.username = need(); break;
|
|
32
|
+
case '--url': opts.uris.push(need()); break;
|
|
33
|
+
case '--notes': opts.notes = need(); break;
|
|
34
|
+
case '--folder': opts.folder = need(); break;
|
|
35
|
+
case '--collection': opts.collection = need(); break;
|
|
36
|
+
case '--organization': opts.organization = need(); break;
|
|
37
|
+
case '--totp': opts.totp = need(); break;
|
|
38
|
+
case '--field': opts.fields.push(parseField(need(), 0)); break;
|
|
39
|
+
case '--field-hidden': opts.fields.push(parseField(need(), 1)); break;
|
|
40
|
+
default:
|
|
41
|
+
if (a.startsWith('--')) throw new Error(`unknown option "${a}"\n${USAGE}`);
|
|
42
|
+
if (name === null) name = a;
|
|
43
|
+
else throw new Error(`unexpected argument "${a}"\n${USAGE}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!opts.json && name === null) throw new Error(USAGE);
|
|
47
|
+
opts.name = name;
|
|
48
|
+
return opts;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Turns a normalized entry into the Bitwarden login-item JSON shape.
|
|
52
|
+
export function buildLoginItem(entry) {
|
|
53
|
+
const item = {
|
|
54
|
+
type: 1,
|
|
55
|
+
name: entry.name,
|
|
56
|
+
notes: entry.notes ?? null,
|
|
57
|
+
login: {
|
|
58
|
+
username: entry.username ?? null,
|
|
59
|
+
password: entry.password ?? null,
|
|
60
|
+
uris: (entry.uris ?? []).filter(Boolean).map((uri) => ({ match: null, uri })),
|
|
61
|
+
totp: entry.totp ?? null,
|
|
62
|
+
},
|
|
63
|
+
fields: (entry.fields ?? []).map((f) => ({ name: f.name, value: f.value ?? '', type: f.type ?? 0 })),
|
|
64
|
+
};
|
|
65
|
+
if (entry.folderId) item.folderId = entry.folderId;
|
|
66
|
+
return item;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function resolveFolderId(bw, session, folderName) {
|
|
70
|
+
const folders = await bw.listFolders({ session });
|
|
71
|
+
const found = folders.find((f) => f.name === folderName && f.id);
|
|
72
|
+
if (found) return found.id;
|
|
73
|
+
const created = await bw.createFolder({ name: folderName, session });
|
|
74
|
+
return created.id;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function resolveCollection(bw, session, collectionName, org) {
|
|
78
|
+
const collections = await bw.listCollections({ session });
|
|
79
|
+
let matches = collections.filter((c) => c.name === collectionName);
|
|
80
|
+
if (org) {
|
|
81
|
+
const orgs = await bw.listOrganizations({ session });
|
|
82
|
+
const match = orgs.find((o) => o.id === org || o.name === org);
|
|
83
|
+
const orgId = match ? match.id : org;
|
|
84
|
+
matches = matches.filter((c) => c.organizationId === orgId);
|
|
85
|
+
}
|
|
86
|
+
if (matches.length === 0) {
|
|
87
|
+
throw new Error(`no collection named "${collectionName}"${org ? ` in organization "${org}"` : ''}`);
|
|
88
|
+
}
|
|
89
|
+
if (matches.length > 1) {
|
|
90
|
+
throw new Error(`multiple collections named "${collectionName}"; disambiguate with --organization <name|id>`);
|
|
91
|
+
}
|
|
92
|
+
return matches[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function runAdd(args, deps) {
|
|
96
|
+
const { keychain, bw, out, err, readStdin } = deps;
|
|
97
|
+
const opts = parseAddArgs(args);
|
|
98
|
+
|
|
99
|
+
const config = await keychain.readConfig();
|
|
100
|
+
if (!config) {
|
|
101
|
+
err('No stored credentials. Run "nbg-pw setup" first.\n');
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let entry;
|
|
106
|
+
if (opts.json) {
|
|
107
|
+
const parsed = JSON.parse(await readStdin());
|
|
108
|
+
if (!parsed.name) throw new Error('json entry requires a "name"');
|
|
109
|
+
entry = parsed;
|
|
110
|
+
} else {
|
|
111
|
+
entry = {
|
|
112
|
+
name: opts.name,
|
|
113
|
+
username: opts.username,
|
|
114
|
+
uris: opts.uris,
|
|
115
|
+
notes: opts.notes,
|
|
116
|
+
totp: opts.totp,
|
|
117
|
+
fields: opts.fields,
|
|
118
|
+
folder: opts.folder,
|
|
119
|
+
collection: opts.collection,
|
|
120
|
+
organization: opts.organization,
|
|
121
|
+
};
|
|
122
|
+
if (opts.passwordStdin) entry.password = (await readStdin()).replace(/\r?\n$/, '');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const session = await bw.ensureSession(config);
|
|
126
|
+
|
|
127
|
+
if (entry.folder) entry.folderId = await resolveFolderId(bw, session, entry.folder);
|
|
128
|
+
|
|
129
|
+
const created = await bw.createItem({ item: buildLoginItem(entry), session });
|
|
130
|
+
|
|
131
|
+
if (entry.collection) {
|
|
132
|
+
const col = await resolveCollection(bw, session, entry.collection, entry.organization);
|
|
133
|
+
await bw.moveItemToCollection({
|
|
134
|
+
itemId: created.id,
|
|
135
|
+
organizationId: col.organizationId,
|
|
136
|
+
collectionIds: [col.id],
|
|
137
|
+
session,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
out(`Created "${created.name}" (${created.id})\n`);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function readStdin() {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
let data = '';
|
|
148
|
+
process.stdin.setEncoding('utf8');
|
|
149
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
150
|
+
process.stdin.on('end', () => resolve(data));
|
|
151
|
+
process.stdin.on('error', reject);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export default async function add(args) {
|
|
156
|
+
return runAdd(args, {
|
|
157
|
+
keychain: keychainModule,
|
|
158
|
+
bw: bwModule,
|
|
159
|
+
readStdin,
|
|
160
|
+
out: (s) => process.stdout.write(s),
|
|
161
|
+
err: (s) => process.stderr.write(s),
|
|
162
|
+
});
|
|
163
|
+
}
|
package/src/commands/get.js
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
}
|
package/src/exec.js
CHANGED
|
@@ -5,6 +5,12 @@ export function run(cmd, args, opts = {}) {
|
|
|
5
5
|
const doTrim = opts.trim !== false;
|
|
6
6
|
return new Promise((resolve, reject) => {
|
|
7
7
|
const child = spawn(cmd, args, { env });
|
|
8
|
+
// `opts.input` is written to the child's stdin (used to pass base64-encoded
|
|
9
|
+
// JSON to `bw create`/`bw move` without exposing secrets on the command line).
|
|
10
|
+
if (opts.input != null) {
|
|
11
|
+
child.stdin.on('error', () => {}); // ignore EPIPE if the child closes stdin early
|
|
12
|
+
child.stdin.end(opts.input);
|
|
13
|
+
}
|
|
8
14
|
let stdout = '';
|
|
9
15
|
let stderr = '';
|
|
10
16
|
child.stdout.on('data', (d) => { stdout += d; });
|