bwx 0.1.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 +101 -0
- package/bin/bwx +3 -0
- package/package.json +28 -0
- package/src/bw/encoding.ts +152 -0
- package/src/bw/runner.ts +60 -0
- package/src/bw/session.ts +187 -0
- package/src/bw/types.ts +112 -0
- package/src/cli/commands/config.ts +103 -0
- package/src/cli/commands/create.ts +133 -0
- package/src/cli/commands/delete.ts +67 -0
- package/src/cli/commands/edit.ts +93 -0
- package/src/cli/commands/field.ts +36 -0
- package/src/cli/commands/get.ts +47 -0
- package/src/cli/commands/lock.ts +15 -0
- package/src/cli/commands/search.ts +85 -0
- package/src/cli/commands/status.ts +36 -0
- package/src/cli/commands/sync.ts +16 -0
- package/src/cli/commands/unlock.ts +15 -0
- package/src/cli/errors.ts +40 -0
- package/src/cli/io.ts +72 -0
- package/src/cli/program.ts +55 -0
- package/src/cli/types.ts +10 -0
- package/src/config/schema.ts +7 -0
- package/src/config/store.ts +21 -0
- package/src/index.ts +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# bwx
|
|
2
|
+
|
|
3
|
+
Bitwarden CLI wrapper for AI agents. Auto-unlock, session caching, structured output.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
The native `bw` CLI requires interactive prompts and manual session management. `bwx` wraps it with:
|
|
8
|
+
|
|
9
|
+
- **Auto-unlock** — first vault command authenticates automatically via macOS Keychain
|
|
10
|
+
- **Session caching** — token persisted to `~/.config/bwx/session` (mode 600), reused across calls
|
|
11
|
+
- **Structured output** — `--json` and `--plain` flags for machine-parseable output
|
|
12
|
+
- **No interaction** — credentials from config + Keychain, never prompts
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun install # or npm install
|
|
18
|
+
bun link # makes `bwx` available globally
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires [Bitwarden CLI](https://bitwarden.com/help/cli/) (`bw`) installed and on PATH.
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Store your Bitwarden account email
|
|
27
|
+
bwx config email your@email.com
|
|
28
|
+
|
|
29
|
+
# Store master password in macOS Keychain
|
|
30
|
+
bwx config master-password
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
### Vault
|
|
36
|
+
|
|
37
|
+
| Command | Description |
|
|
38
|
+
|---------|-------------|
|
|
39
|
+
| `bwx status` | Show vault state |
|
|
40
|
+
| `bwx unlock` | Unlock vault & cache session |
|
|
41
|
+
| `bwx lock` | Lock vault & clear session |
|
|
42
|
+
| `bwx sync` | Sync vault from server |
|
|
43
|
+
|
|
44
|
+
### Read
|
|
45
|
+
|
|
46
|
+
| Command | Description |
|
|
47
|
+
|---------|-------------|
|
|
48
|
+
| `bwx get <field> <item>` | Get field (`password`, `username`, `totp`, `notes`, `uri`, `item`) |
|
|
49
|
+
| `bwx field <item> <name>` | Get custom field by name |
|
|
50
|
+
| `bwx search <query>` | Search items (`--type`, `--folder`, `--limit`) |
|
|
51
|
+
|
|
52
|
+
### Write
|
|
53
|
+
|
|
54
|
+
| Command | Description |
|
|
55
|
+
|---------|-------------|
|
|
56
|
+
| `bwx create` | Create item (`--type`, `--name`, `--username`, `--password`, `--uri`, `--field k=v`) |
|
|
57
|
+
| `bwx edit <item>` | Edit item (`--name`, `--password`, `--add-field k=v`, `--rm-field name`) |
|
|
58
|
+
| `bwx delete <item>` | Delete item (`--permanent`, `--force`) |
|
|
59
|
+
|
|
60
|
+
### Config
|
|
61
|
+
|
|
62
|
+
| Command | Description |
|
|
63
|
+
|---------|-------------|
|
|
64
|
+
| `bwx config email [addr]` | Set account email |
|
|
65
|
+
| `bwx config master-password` | Store master password in Keychain |
|
|
66
|
+
|
|
67
|
+
### Global flags
|
|
68
|
+
|
|
69
|
+
| Flag | Effect |
|
|
70
|
+
|------|--------|
|
|
71
|
+
| `--json` | JSON output: `{ "data": ... }` / `{ "error": ... }` |
|
|
72
|
+
| `--plain` | Tab-separated, one item per line |
|
|
73
|
+
| `-q, --quiet` | Suppress logs |
|
|
74
|
+
| `-v, --verbose` | Include stack traces |
|
|
75
|
+
|
|
76
|
+
## Custom fields
|
|
77
|
+
|
|
78
|
+
Field syntax for `--field` / `--add-field`:
|
|
79
|
+
|
|
80
|
+
- `key=value` — text field
|
|
81
|
+
- `!key=value` — hidden field
|
|
82
|
+
- `bool:key=value` — boolean field
|
|
83
|
+
|
|
84
|
+
## Exit codes
|
|
85
|
+
|
|
86
|
+
| Code | Meaning |
|
|
87
|
+
|------|---------|
|
|
88
|
+
| 0 | OK |
|
|
89
|
+
| 1 | Unknown error |
|
|
90
|
+
| 2 | Bad arguments |
|
|
91
|
+
| 3 | Auth failed |
|
|
92
|
+
| 4 | Not found |
|
|
93
|
+
| 5 | bw error |
|
|
94
|
+
| 6 | Network error |
|
|
95
|
+
| 7 | Config error |
|
|
96
|
+
| 8 | Timeout |
|
|
97
|
+
| 9 | User cancelled |
|
|
98
|
+
|
|
99
|
+
## Stack
|
|
100
|
+
|
|
101
|
+
Bun, TypeScript, Commander, Zod, picocolors.
|
package/bin/bwx
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bwx",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"bin",
|
|
7
|
+
"src"
|
|
8
|
+
],
|
|
9
|
+
"bin": {
|
|
10
|
+
"bwx": "bin/bwx"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
14
|
+
"test": "bun test",
|
|
15
|
+
"cmd": "bun run bin/bwx"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/bun": "latest"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"typescript": "^5"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"commander": "^14.0.3",
|
|
25
|
+
"picocolors": "^1.1.1",
|
|
26
|
+
"zod": "^4.3.6"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { BwField, BwFieldType, BwItem, BwItemType, BwUri } from "./types.ts";
|
|
2
|
+
import { BwFieldType as FT, BwItemType as IT } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
// --- Field flag parsing ---
|
|
5
|
+
// k=v → Text field
|
|
6
|
+
// !k=v → Hidden field
|
|
7
|
+
// bool:k=v → Boolean field
|
|
8
|
+
|
|
9
|
+
export interface ParsedField {
|
|
10
|
+
name: string;
|
|
11
|
+
value: string;
|
|
12
|
+
type: BwFieldType;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseFieldFlag(raw: string): ParsedField {
|
|
16
|
+
if (raw.startsWith("bool:")) {
|
|
17
|
+
const rest = raw.slice(5);
|
|
18
|
+
const eq = rest.indexOf("=");
|
|
19
|
+
if (eq === -1) return { name: rest, value: "true", type: FT.Boolean };
|
|
20
|
+
return { name: rest.slice(0, eq), value: rest.slice(eq + 1), type: FT.Boolean };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (raw.startsWith("!")) {
|
|
24
|
+
const rest = raw.slice(1);
|
|
25
|
+
const eq = rest.indexOf("=");
|
|
26
|
+
if (eq === -1) return { name: rest, value: "", type: FT.Hidden };
|
|
27
|
+
return { name: rest.slice(0, eq), value: rest.slice(eq + 1), type: FT.Hidden };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const eq = raw.indexOf("=");
|
|
31
|
+
if (eq === -1) return { name: raw, value: "", type: FT.Text };
|
|
32
|
+
return { name: raw.slice(0, eq), value: raw.slice(eq + 1), type: FT.Text };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- Build new item JSON ---
|
|
36
|
+
|
|
37
|
+
export interface NewItemOptions {
|
|
38
|
+
type?: "login" | "note";
|
|
39
|
+
name: string;
|
|
40
|
+
notes?: string | null;
|
|
41
|
+
username?: string | null;
|
|
42
|
+
password?: string | null;
|
|
43
|
+
uris?: string[];
|
|
44
|
+
fields?: ParsedField[];
|
|
45
|
+
folderId?: string | null;
|
|
46
|
+
favorite?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildNewItem(options: NewItemOptions): Record<string, unknown> {
|
|
50
|
+
const isLogin = options.type === "login";
|
|
51
|
+
const itemType: BwItemType = isLogin ? IT.Login : IT.SecureNote;
|
|
52
|
+
|
|
53
|
+
const item: Record<string, unknown> = {
|
|
54
|
+
organizationId: null,
|
|
55
|
+
collectionIds: null,
|
|
56
|
+
folderId: options.folderId ?? null,
|
|
57
|
+
type: itemType,
|
|
58
|
+
name: options.name,
|
|
59
|
+
notes: options.notes ?? null,
|
|
60
|
+
favorite: options.favorite ?? false,
|
|
61
|
+
reprompt: 0,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (isLogin) {
|
|
65
|
+
item.login = {
|
|
66
|
+
username: options.username ?? null,
|
|
67
|
+
password: options.password ?? null,
|
|
68
|
+
totp: null,
|
|
69
|
+
uris: options.uris?.length
|
|
70
|
+
? options.uris.map((uri) => ({ match: null, uri }))
|
|
71
|
+
: null,
|
|
72
|
+
};
|
|
73
|
+
} else {
|
|
74
|
+
item.secureNote = { type: 0 };
|
|
75
|
+
item.login = null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (options.fields?.length) {
|
|
79
|
+
item.fields = options.fields.map((f) => ({
|
|
80
|
+
name: f.name,
|
|
81
|
+
value: f.value,
|
|
82
|
+
type: f.type,
|
|
83
|
+
}));
|
|
84
|
+
} else {
|
|
85
|
+
item.fields = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return item;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- Patch existing item ---
|
|
92
|
+
|
|
93
|
+
export interface PatchOptions {
|
|
94
|
+
name?: string;
|
|
95
|
+
notes?: string;
|
|
96
|
+
username?: string;
|
|
97
|
+
password?: string;
|
|
98
|
+
uris?: string[];
|
|
99
|
+
addFields?: ParsedField[];
|
|
100
|
+
rmFields?: string[];
|
|
101
|
+
folderId?: string;
|
|
102
|
+
favorite?: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function patchItem(
|
|
106
|
+
item: Record<string, unknown>,
|
|
107
|
+
patch: PatchOptions,
|
|
108
|
+
): Record<string, unknown> {
|
|
109
|
+
const result = { ...item };
|
|
110
|
+
|
|
111
|
+
if (patch.name !== undefined) result.name = patch.name;
|
|
112
|
+
if (patch.notes !== undefined) result.notes = patch.notes;
|
|
113
|
+
if (patch.folderId !== undefined) result.folderId = patch.folderId;
|
|
114
|
+
if (patch.favorite !== undefined) result.favorite = patch.favorite;
|
|
115
|
+
|
|
116
|
+
// Login fields
|
|
117
|
+
if (patch.username !== undefined || patch.password !== undefined || patch.uris !== undefined) {
|
|
118
|
+
const login = (result.login as Record<string, unknown>) ?? {};
|
|
119
|
+
if (patch.username !== undefined) login.username = patch.username;
|
|
120
|
+
if (patch.password !== undefined) login.password = patch.password;
|
|
121
|
+
if (patch.uris !== undefined) {
|
|
122
|
+
login.uris = patch.uris.map((uri) => ({ match: null, uri }));
|
|
123
|
+
}
|
|
124
|
+
result.login = login;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Fields: remove then add
|
|
128
|
+
let fields = Array.isArray(result.fields) ? [...(result.fields as BwField[])] : [];
|
|
129
|
+
if (patch.rmFields?.length) {
|
|
130
|
+
const removeSet = new Set(patch.rmFields);
|
|
131
|
+
fields = fields.filter((f) => !removeSet.has(f.name));
|
|
132
|
+
}
|
|
133
|
+
if (patch.addFields?.length) {
|
|
134
|
+
for (const f of patch.addFields) {
|
|
135
|
+
const idx = fields.findIndex((e) => e.name === f.name);
|
|
136
|
+
if (idx >= 0) {
|
|
137
|
+
fields[idx] = { name: f.name, value: f.value, type: f.type };
|
|
138
|
+
} else {
|
|
139
|
+
fields.push({ name: f.name, value: f.value, type: f.type });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
result.fields = fields.length > 0 ? fields : null;
|
|
144
|
+
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Base64 encoding (replaces `bw encode`) ---
|
|
149
|
+
|
|
150
|
+
export function bwEncode(input: string): string {
|
|
151
|
+
return Buffer.from(input).toString("base64");
|
|
152
|
+
}
|
package/src/bw/runner.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CliError, ExitCode, classifyBwError } from "../cli/errors.ts";
|
|
2
|
+
|
|
3
|
+
export interface BwResult {
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
exitCode: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let sessionToken: string | null = null;
|
|
10
|
+
|
|
11
|
+
export function setSession(token: string | null): void {
|
|
12
|
+
sessionToken = token;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getSession(): string | null {
|
|
16
|
+
return sessionToken;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function runBw(
|
|
20
|
+
args: string[],
|
|
21
|
+
options?: { session?: string | null; env?: Record<string, string> },
|
|
22
|
+
): Promise<BwResult> {
|
|
23
|
+
const session = options?.session ?? sessionToken;
|
|
24
|
+
const fullArgs = session ? [...args, "--session", session] : args;
|
|
25
|
+
|
|
26
|
+
const proc = Bun.spawn(["bw", ...fullArgs], {
|
|
27
|
+
stdout: "pipe",
|
|
28
|
+
stderr: "pipe",
|
|
29
|
+
stdin: "ignore",
|
|
30
|
+
env: options?.env ? { ...process.env, ...options.env } : undefined,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const [stdout, stderr] = await Promise.all([
|
|
34
|
+
new Response(proc.stdout).text(),
|
|
35
|
+
new Response(proc.stderr).text(),
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const exitCode = await proc.exited;
|
|
39
|
+
|
|
40
|
+
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function runBwOrThrow(
|
|
44
|
+
args: string[],
|
|
45
|
+
options?: { session?: string | null; env?: Record<string, string> },
|
|
46
|
+
): Promise<string> {
|
|
47
|
+
const result = await runBw(args, options);
|
|
48
|
+
if (result.exitCode !== 0) {
|
|
49
|
+
const code = classifyBwError(result.stderr);
|
|
50
|
+
throw new CliError(
|
|
51
|
+
result.stderr || `bw ${args[0]} failed (exit ${result.exitCode})`,
|
|
52
|
+
code,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return result.stdout;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isAuthError(result: BwResult): boolean {
|
|
59
|
+
return /not logged in|vault is locked|unauthenticated/i.test(result.stderr);
|
|
60
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { CliError, ExitCode } from "../cli/errors.ts";
|
|
2
|
+
import type { GlobalOptions } from "../cli/types.ts";
|
|
3
|
+
import { emitLog } from "../cli/io.ts";
|
|
4
|
+
import {
|
|
5
|
+
runBw,
|
|
6
|
+
runBwOrThrow,
|
|
7
|
+
setSession,
|
|
8
|
+
getSession,
|
|
9
|
+
isAuthError,
|
|
10
|
+
type BwResult,
|
|
11
|
+
} from "./runner.ts";
|
|
12
|
+
import { BwStatusSchema, type BwStatus } from "./types.ts";
|
|
13
|
+
import { loadConfig } from "../config/store.ts";
|
|
14
|
+
|
|
15
|
+
const SESSION_PATH = `${process.env.HOME}/.config/bwx/session`;
|
|
16
|
+
|
|
17
|
+
async function loadCachedSession(): Promise<string | null> {
|
|
18
|
+
try {
|
|
19
|
+
const token = await Bun.file(SESSION_PATH).text();
|
|
20
|
+
return token.trim() || null;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function saveCachedSession(token: string): Promise<void> {
|
|
27
|
+
const dir = SESSION_PATH.replace(/\/[^/]+$/, "");
|
|
28
|
+
const { mkdirSync } = await import("node:fs");
|
|
29
|
+
mkdirSync(dir, { recursive: true });
|
|
30
|
+
await Bun.write(SESSION_PATH, token);
|
|
31
|
+
const proc = Bun.spawn(["chmod", "600", SESSION_PATH]);
|
|
32
|
+
await proc.exited;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function clearCachedSession(): Promise<void> {
|
|
36
|
+
try {
|
|
37
|
+
const { unlinkSync } = await import("node:fs");
|
|
38
|
+
unlinkSync(SESSION_PATH);
|
|
39
|
+
} catch {
|
|
40
|
+
// Already gone
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function getStatus(): Promise<BwStatus> {
|
|
45
|
+
const result = await runBw(["status"], { session: null });
|
|
46
|
+
try {
|
|
47
|
+
return BwStatusSchema.parse(JSON.parse(result.stdout));
|
|
48
|
+
} catch {
|
|
49
|
+
throw new CliError(
|
|
50
|
+
`Failed to parse bw status: ${result.stdout}`,
|
|
51
|
+
ExitCode.BwError,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function getMasterPassword(): Promise<string> {
|
|
57
|
+
const proc = Bun.spawn(
|
|
58
|
+
[
|
|
59
|
+
"security",
|
|
60
|
+
"find-generic-password",
|
|
61
|
+
"-a",
|
|
62
|
+
"bitwarden",
|
|
63
|
+
"-s",
|
|
64
|
+
"bitwarden-master",
|
|
65
|
+
"-w",
|
|
66
|
+
],
|
|
67
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
68
|
+
);
|
|
69
|
+
const pw = (await new Response(proc.stdout).text()).trim();
|
|
70
|
+
const exitCode = await proc.exited;
|
|
71
|
+
if (exitCode !== 0 || !pw) {
|
|
72
|
+
throw new CliError(
|
|
73
|
+
"Master password not found in Keychain",
|
|
74
|
+
ExitCode.AuthFailed,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return pw;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function ensureUnlocked(
|
|
81
|
+
opts: GlobalOptions,
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
// Try cached session first
|
|
84
|
+
const cached = getSession() || (await loadCachedSession());
|
|
85
|
+
if (cached) {
|
|
86
|
+
setSession(cached);
|
|
87
|
+
// Quick check — try a lightweight command
|
|
88
|
+
const check = await runBw(["status"]);
|
|
89
|
+
if (check.exitCode === 0) {
|
|
90
|
+
try {
|
|
91
|
+
const status = BwStatusSchema.parse(JSON.parse(check.stdout));
|
|
92
|
+
if (status.status === "unlocked") return;
|
|
93
|
+
} catch {
|
|
94
|
+
// Fall through
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const status = await getStatus();
|
|
100
|
+
|
|
101
|
+
if (status.status === "unlocked") {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (status.status === "unauthenticated") {
|
|
106
|
+
const config = await loadConfig();
|
|
107
|
+
if (!config.email) {
|
|
108
|
+
throw new CliError(
|
|
109
|
+
"Email not configured. Run: bwx config email",
|
|
110
|
+
ExitCode.AuthFailed,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const pw = await getMasterPassword();
|
|
115
|
+
emitLog("Logging in...", opts);
|
|
116
|
+
const loginResult = await runBw(
|
|
117
|
+
[
|
|
118
|
+
"login",
|
|
119
|
+
config.email,
|
|
120
|
+
"--passwordenv",
|
|
121
|
+
"BW_MASTER_PW",
|
|
122
|
+
"--quiet",
|
|
123
|
+
],
|
|
124
|
+
{
|
|
125
|
+
session: null,
|
|
126
|
+
env: { BW_MASTER_PW: pw, BW_NOINTERACTION: "true" },
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
if (loginResult.exitCode !== 0) {
|
|
130
|
+
throw new CliError(
|
|
131
|
+
`Login failed: ${loginResult.stderr}`,
|
|
132
|
+
ExitCode.AuthFailed,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Unlock
|
|
138
|
+
const pw = await getMasterPassword();
|
|
139
|
+
emitLog("Unlocking vault...", opts);
|
|
140
|
+
const unlockResult = await runBw(
|
|
141
|
+
["unlock", "--passwordenv", "BW_MASTER_PW", "--raw"],
|
|
142
|
+
{ session: null, env: { BW_MASTER_PW: pw } },
|
|
143
|
+
);
|
|
144
|
+
if (unlockResult.exitCode !== 0 || !unlockResult.stdout) {
|
|
145
|
+
throw new CliError(
|
|
146
|
+
`Unlock failed: ${unlockResult.stderr}`,
|
|
147
|
+
ExitCode.AuthFailed,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const token = unlockResult.stdout;
|
|
152
|
+
setSession(token);
|
|
153
|
+
await saveCachedSession(token);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function lockVault(): Promise<void> {
|
|
157
|
+
await runBw(["lock"], { session: null });
|
|
158
|
+
setSession(null);
|
|
159
|
+
await clearCachedSession();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function withSession<T>(
|
|
163
|
+
opts: GlobalOptions,
|
|
164
|
+
fn: () => Promise<T>,
|
|
165
|
+
): Promise<T> {
|
|
166
|
+
// Try with current/cached session
|
|
167
|
+
const cached = getSession() || (await loadCachedSession());
|
|
168
|
+
if (cached) {
|
|
169
|
+
setSession(cached);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
return await fn();
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (
|
|
176
|
+
err instanceof CliError &&
|
|
177
|
+
err.exitCode === ExitCode.AuthFailed
|
|
178
|
+
) {
|
|
179
|
+
// Clear stale session and re-auth
|
|
180
|
+
setSession(null);
|
|
181
|
+
await clearCachedSession();
|
|
182
|
+
await ensureUnlocked(opts);
|
|
183
|
+
return await fn();
|
|
184
|
+
}
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
}
|
package/src/bw/types.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
|
|
3
|
+
// --- Vault status ---
|
|
4
|
+
|
|
5
|
+
export const BwStatusSchema = z.object({
|
|
6
|
+
serverUrl: z.string().nullable(),
|
|
7
|
+
lastSync: z.string().nullable(),
|
|
8
|
+
userEmail: z.string().nullable().optional(),
|
|
9
|
+
userId: z.string().nullable().optional(),
|
|
10
|
+
status: z.enum(["unlocked", "locked", "unauthenticated"]),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export type BwStatus = z.infer<typeof BwStatusSchema>;
|
|
14
|
+
|
|
15
|
+
// --- Item types ---
|
|
16
|
+
|
|
17
|
+
export const BwItemType = {
|
|
18
|
+
Login: 1,
|
|
19
|
+
SecureNote: 2,
|
|
20
|
+
Card: 3,
|
|
21
|
+
Identity: 4,
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
export type BwItemType = (typeof BwItemType)[keyof typeof BwItemType];
|
|
25
|
+
|
|
26
|
+
export const BW_TYPE_LABELS: Record<number, string> = {
|
|
27
|
+
1: "login",
|
|
28
|
+
2: "note",
|
|
29
|
+
3: "card",
|
|
30
|
+
4: "identity",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const BW_TYPE_FROM_NAME: Record<string, BwItemType> = {
|
|
34
|
+
login: BwItemType.Login,
|
|
35
|
+
note: BwItemType.SecureNote,
|
|
36
|
+
card: BwItemType.Card,
|
|
37
|
+
identity: BwItemType.Identity,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// --- Field types ---
|
|
41
|
+
|
|
42
|
+
export const BwFieldType = {
|
|
43
|
+
Text: 0,
|
|
44
|
+
Hidden: 1,
|
|
45
|
+
Boolean: 2,
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
export type BwFieldType = (typeof BwFieldType)[keyof typeof BwFieldType];
|
|
49
|
+
|
|
50
|
+
export interface BwField {
|
|
51
|
+
name: string;
|
|
52
|
+
value: string;
|
|
53
|
+
type: BwFieldType;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- URI ---
|
|
57
|
+
|
|
58
|
+
export interface BwUri {
|
|
59
|
+
match: null;
|
|
60
|
+
uri: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- Login ---
|
|
64
|
+
|
|
65
|
+
export interface BwLogin {
|
|
66
|
+
username: string | null;
|
|
67
|
+
password: string | null;
|
|
68
|
+
totp: string | null;
|
|
69
|
+
uris: BwUri[] | null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --- Item (loose schema — passthrough for edit round-trip) ---
|
|
73
|
+
|
|
74
|
+
export const BwItemSchema = z
|
|
75
|
+
.object({
|
|
76
|
+
id: z.string(),
|
|
77
|
+
organizationId: z.string().nullable(),
|
|
78
|
+
folderId: z.string().nullable(),
|
|
79
|
+
type: z.number(),
|
|
80
|
+
name: z.string(),
|
|
81
|
+
notes: z.string().nullable(),
|
|
82
|
+
favorite: z.boolean(),
|
|
83
|
+
fields: z
|
|
84
|
+
.array(
|
|
85
|
+
z.object({
|
|
86
|
+
name: z.string(),
|
|
87
|
+
value: z.string().nullable(),
|
|
88
|
+
type: z.number(),
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
91
|
+
.nullable(),
|
|
92
|
+
login: z
|
|
93
|
+
.object({
|
|
94
|
+
username: z.string().nullable(),
|
|
95
|
+
password: z.string().nullable(),
|
|
96
|
+
totp: z.string().nullable(),
|
|
97
|
+
uris: z
|
|
98
|
+
.array(
|
|
99
|
+
z.object({
|
|
100
|
+
match: z.number().nullable().optional(),
|
|
101
|
+
uri: z.string(),
|
|
102
|
+
}),
|
|
103
|
+
)
|
|
104
|
+
.nullable(),
|
|
105
|
+
})
|
|
106
|
+
.nullable()
|
|
107
|
+
.optional(),
|
|
108
|
+
reprompt: z.number().optional(),
|
|
109
|
+
})
|
|
110
|
+
.passthrough();
|
|
111
|
+
|
|
112
|
+
export type BwItem = z.infer<typeof BwItemSchema>;
|