bitty-tui 0.0.20 → 0.0.22
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/dist/cli.js +2 -0
- package/dist/clients/bw.js +73 -17
- package/dist/components/TextInput.js +1 -1
- package/dist/dashboard/DashboardView.js +1 -1
- package/dist/dashboard/components/VaultList.js +3 -2
- package/dist/debug.d.ts +3 -0
- package/dist/debug.js +15 -0
- package/package.json +9 -9
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { readPackageUpSync } from "read-package-up";
|
|
|
8
8
|
import { art } from "./theme/art.js";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { debugLogPath } from "./debug.js";
|
|
11
12
|
const args = process.argv.slice(2);
|
|
12
13
|
if (args.includes("--version") || args.includes("-v")) {
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -24,6 +25,7 @@ if (args.includes("--help") || args.includes("-h")) {
|
|
|
24
25
|
Options
|
|
25
26
|
--help Show help
|
|
26
27
|
--version Show version
|
|
28
|
+
--debug Write API debug logs to ${debugLogPath}
|
|
27
29
|
`);
|
|
28
30
|
process.exit(0);
|
|
29
31
|
}
|
package/dist/clients/bw.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import crypto from "node:crypto";
|
|
21
21
|
import * as argon2 from "argon2";
|
|
22
|
+
import { debugEnabled, debugLog } from "../debug.js";
|
|
22
23
|
export class FetchError extends Error {
|
|
23
24
|
status;
|
|
24
25
|
data;
|
|
@@ -32,7 +33,26 @@ export class FetchError extends Error {
|
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
const fetchApi = async (...args) => {
|
|
36
|
+
if (debugEnabled) {
|
|
37
|
+
const [input, init] = args;
|
|
38
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
39
|
+
const url = input instanceof Request ? input.url : String(input);
|
|
40
|
+
const body = init?.body != null ? String(init.body) : "(none)";
|
|
41
|
+
const headers = new Headers(init?.headers);
|
|
42
|
+
const headersStr = [...headers.entries()]
|
|
43
|
+
.map(([k, v]) => ` ${k}: ${v}`)
|
|
44
|
+
.join("\n");
|
|
45
|
+
debugLog(`→ ${method} ${url}`);
|
|
46
|
+
debugLog(` headers:\n${headersStr || " (none)"}`);
|
|
47
|
+
debugLog(` body: ${body}`);
|
|
48
|
+
}
|
|
35
49
|
const response = await fetch(...args);
|
|
50
|
+
if (debugEnabled) {
|
|
51
|
+
const clone = response.clone();
|
|
52
|
+
const responseBody = await clone.text();
|
|
53
|
+
debugLog(`← ${response.status} ${response.statusText}`);
|
|
54
|
+
debugLog(` response: ${responseBody}`);
|
|
55
|
+
}
|
|
36
56
|
if (!response.ok) {
|
|
37
57
|
const data = await response.text();
|
|
38
58
|
throw new FetchError(response.status, data);
|
|
@@ -73,6 +93,17 @@ export var KdfType;
|
|
|
73
93
|
KdfType[KdfType["Argon2id"] = 1] = "Argon2id";
|
|
74
94
|
})(KdfType || (KdfType = {}));
|
|
75
95
|
const DEVICE_IDENTIFIER = "928f9664-5559-4a7b-9853-caf5bfa5dd57";
|
|
96
|
+
const DEVICE_TYPE = "9"; // ChromeBrowser → ClientName::Web in the SDK
|
|
97
|
+
const CLIENT_NAME = "web";
|
|
98
|
+
const CLIENT_VERSION = "2025.9.0";
|
|
99
|
+
const USER_AGENT = "Bitwarden_Bitty";
|
|
100
|
+
const defaultHeaders = () => ({
|
|
101
|
+
"Device-Identifier": DEVICE_IDENTIFIER,
|
|
102
|
+
"Bitwarden-Client-Name": CLIENT_NAME,
|
|
103
|
+
"Bitwarden-Client-Version": CLIENT_VERSION,
|
|
104
|
+
"Device-Type": DEVICE_TYPE,
|
|
105
|
+
"User-Agent": USER_AGENT,
|
|
106
|
+
});
|
|
76
107
|
class Bw {
|
|
77
108
|
/**
|
|
78
109
|
* Derives the master key and related keys from the user's email and password.
|
|
@@ -86,12 +117,13 @@ class Bw {
|
|
|
86
117
|
* The encryption key will be used to decrypt the user keys.
|
|
87
118
|
*/
|
|
88
119
|
async deriveMasterKey(email, password, prelogin) {
|
|
120
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
89
121
|
let masterKey;
|
|
90
122
|
if (prelogin.kdf === KdfType.PBKDF2) {
|
|
91
|
-
masterKey = this.derivePbkdf2(password,
|
|
123
|
+
masterKey = this.derivePbkdf2(password, normalizedEmail, prelogin.kdfIterations);
|
|
92
124
|
}
|
|
93
125
|
else {
|
|
94
|
-
masterKey = await this.deriveArgon2(password,
|
|
126
|
+
masterKey = await this.deriveArgon2(password, normalizedEmail, prelogin.kdfIterations, prelogin.kdfMemory, prelogin.kdfParallelism);
|
|
95
127
|
}
|
|
96
128
|
const masterPasswordHashBytes = this.derivePbkdf2(masterKey, password, 1);
|
|
97
129
|
const masterPasswordHash = Buffer.from(masterPasswordHashBytes).toString("base64");
|
|
@@ -343,8 +375,9 @@ export class Client {
|
|
|
343
375
|
}
|
|
344
376
|
setUrls(uri) {
|
|
345
377
|
if (uri.baseUrl) {
|
|
346
|
-
|
|
347
|
-
this.
|
|
378
|
+
const base = uri.baseUrl.replace(/\/+$/, "");
|
|
379
|
+
this.apiUrl = base + "/api";
|
|
380
|
+
this.identityUrl = base + "/identity";
|
|
348
381
|
}
|
|
349
382
|
else {
|
|
350
383
|
this.apiUrl = uri.apiUrl;
|
|
@@ -371,6 +404,8 @@ export class Client {
|
|
|
371
404
|
const prelogin = await fetchApi(`${this.identityUrl}/accounts/prelogin`, {
|
|
372
405
|
method: "POST",
|
|
373
406
|
headers: {
|
|
407
|
+
...defaultHeaders(),
|
|
408
|
+
Accept: "application/json",
|
|
374
409
|
"Content-Type": "application/json",
|
|
375
410
|
},
|
|
376
411
|
body: JSON.stringify({ email }),
|
|
@@ -379,24 +414,24 @@ export class Client {
|
|
|
379
414
|
this.keys = keys;
|
|
380
415
|
}
|
|
381
416
|
const bodyParams = new URLSearchParams();
|
|
382
|
-
bodyParams.append("
|
|
383
|
-
bodyParams.append("
|
|
384
|
-
bodyParams.append("grant_type", "password");
|
|
385
|
-
bodyParams.append("deviceName", "chrome");
|
|
417
|
+
bodyParams.append("scope", "api offline_access");
|
|
418
|
+
bodyParams.append("client_id", "web");
|
|
386
419
|
bodyParams.append("deviceType", "9");
|
|
387
420
|
bodyParams.append("deviceIdentifier", DEVICE_IDENTIFIER);
|
|
388
|
-
bodyParams.append("
|
|
389
|
-
bodyParams.append("
|
|
421
|
+
bodyParams.append("deviceName", "firefox");
|
|
422
|
+
bodyParams.append("grant_type", "password");
|
|
423
|
+
bodyParams.append("username", email);
|
|
424
|
+
bodyParams.append("password", keys.masterPasswordHash);
|
|
390
425
|
for (const [key, value] of Object.entries(opts || {})) {
|
|
391
426
|
bodyParams.append(key, value);
|
|
392
427
|
}
|
|
393
428
|
const identityReq = await fetchApi(`${this.identityUrl}/connect/token`, {
|
|
394
429
|
method: "POST",
|
|
395
430
|
headers: {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
"
|
|
399
|
-
|
|
431
|
+
...defaultHeaders(),
|
|
432
|
+
Accept: "application/json",
|
|
433
|
+
"Cache-Control": "no-store",
|
|
434
|
+
Pragma: "no-cache",
|
|
400
435
|
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
|
401
436
|
},
|
|
402
437
|
body: bodyParams.toString(),
|
|
@@ -419,6 +454,8 @@ export class Client {
|
|
|
419
454
|
return fetchApi(`${this.apiUrl}/two-factor/send-email-login`, {
|
|
420
455
|
method: "POST",
|
|
421
456
|
headers: {
|
|
457
|
+
...defaultHeaders(),
|
|
458
|
+
Accept: "application/json",
|
|
422
459
|
"Content-Type": "application/json",
|
|
423
460
|
},
|
|
424
461
|
body: JSON.stringify({
|
|
@@ -437,12 +474,20 @@ export class Client {
|
|
|
437
474
|
if (!this.refreshToken) {
|
|
438
475
|
throw new Error("No refresh token available. Please login first.");
|
|
439
476
|
}
|
|
477
|
+
const bodyParams = new URLSearchParams();
|
|
478
|
+
bodyParams.append("grant_type", "refresh_token");
|
|
479
|
+
bodyParams.append("refresh_token", this.refreshToken);
|
|
480
|
+
bodyParams.append("client_id", "web");
|
|
440
481
|
const identityReq = await fetchApi(`${this.identityUrl}/connect/token`, {
|
|
441
482
|
method: "POST",
|
|
442
483
|
headers: {
|
|
443
|
-
|
|
484
|
+
...defaultHeaders(),
|
|
485
|
+
Accept: "application/json",
|
|
486
|
+
"Cache-Control": "no-store",
|
|
487
|
+
Pragma: "no-cache",
|
|
488
|
+
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
|
444
489
|
},
|
|
445
|
-
body:
|
|
490
|
+
body: bodyParams.toString(),
|
|
446
491
|
}).then((r) => r.json());
|
|
447
492
|
this.token = identityReq.access_token;
|
|
448
493
|
this.refreshToken = identityReq.refresh_token;
|
|
@@ -459,8 +504,9 @@ export class Client {
|
|
|
459
504
|
this.syncCache = await fetchApi(`${this.apiUrl}/sync?excludeDomains=true`, {
|
|
460
505
|
method: "GET",
|
|
461
506
|
headers: {
|
|
507
|
+
...defaultHeaders(),
|
|
508
|
+
Accept: "application/json",
|
|
462
509
|
Authorization: `Bearer ${this.token}`,
|
|
463
|
-
"bitwarden-client-version": "2025.9.0",
|
|
464
510
|
},
|
|
465
511
|
}).then((r) => r.json());
|
|
466
512
|
this.decryptOrgKeys();
|
|
@@ -631,6 +677,8 @@ export class Client {
|
|
|
631
677
|
const s = await fetchApi(url, {
|
|
632
678
|
method: "POST",
|
|
633
679
|
headers: {
|
|
680
|
+
// ...defaultHeaders(),
|
|
681
|
+
// Accept: "application/json",
|
|
634
682
|
Authorization: `Bearer ${this.token}`,
|
|
635
683
|
"Content-Type": "application/json",
|
|
636
684
|
},
|
|
@@ -678,6 +726,8 @@ export class Client {
|
|
|
678
726
|
const s = await fetchApi(`${this.apiUrl}/ciphers/${id}`, {
|
|
679
727
|
method: "PUT",
|
|
680
728
|
headers: {
|
|
729
|
+
// ...defaultHeaders(),
|
|
730
|
+
// Accept: "application/json",
|
|
681
731
|
Authorization: `Bearer ${this.token}`,
|
|
682
732
|
"Content-Type": "application/json",
|
|
683
733
|
},
|
|
@@ -692,6 +742,8 @@ export class Client {
|
|
|
692
742
|
await fetchApi(`${this.apiUrl}/ciphers/${id}`, {
|
|
693
743
|
method: "DELETE",
|
|
694
744
|
headers: {
|
|
745
|
+
// ...defaultHeaders(),
|
|
746
|
+
// Accept: "application/json",
|
|
695
747
|
Authorization: `Bearer ${this.token}`,
|
|
696
748
|
},
|
|
697
749
|
});
|
|
@@ -715,6 +767,8 @@ export class Client {
|
|
|
715
767
|
const s = await fetchApi(`${this.apiUrl}/ciphers/${id}/share`, {
|
|
716
768
|
method: "PUT",
|
|
717
769
|
headers: {
|
|
770
|
+
// ...defaultHeaders(),
|
|
771
|
+
// Accept: "application/json",
|
|
718
772
|
Authorization: `Bearer ${this.token}`,
|
|
719
773
|
"Content-Type": "application/json",
|
|
720
774
|
},
|
|
@@ -732,6 +786,8 @@ export class Client {
|
|
|
732
786
|
const s = await fetchApi(`${this.apiUrl}/ciphers/${id}/collections_v2`, {
|
|
733
787
|
method: "PUT",
|
|
734
788
|
headers: {
|
|
789
|
+
// ...defaultHeaders(),
|
|
790
|
+
// Accept: "application/json",
|
|
735
791
|
Authorization: `Bearer ${this.token}`,
|
|
736
792
|
"Content-Type": "application/json",
|
|
737
793
|
},
|
|
@@ -138,7 +138,7 @@ export function DashboardView({ onLogout }) {
|
|
|
138
138
|
setShowDetails(true);
|
|
139
139
|
setTimeout(focusNext, 50);
|
|
140
140
|
}, [showDetails]);
|
|
141
|
-
return (_jsxs(Box, { flexDirection: "column", width: "100%", height: stdout.rows - 2, children: [_jsx(Box, { borderStyle: "double", borderColor: primary, paddingX: 2, justifyContent: "center", flexShrink: 0, children: _jsx(Text, { bold: true, color: primary, children: "BiTTY" }) }), _jsxs(Box, { children: [_jsx(Box, { width: "40%", children: _jsx(TextInput, { id: "search", placeholder: focusedComponent === "search" ? "" : "[/] Search in vault", value: searchQuery, isActive: false, onChange: setSearchQuery, onSubmit: () => {
|
|
141
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", height: stdout.rows - 2, children: [_jsx(Box, { borderStyle: "double", borderColor: primary, paddingX: 2, justifyContent: "center", flexShrink: 0, children: _jsx(Text, { bold: true, color: primary, children: "BiTTY" }) }), _jsxs(Box, { flexShrink: 0, children: [_jsx(Box, { width: "40%", children: _jsx(TextInput, { id: "search", placeholder: focusedComponent === "search" ? "" : "[/] Search in vault", value: searchQuery, isActive: false, onChange: setSearchQuery, onSubmit: () => {
|
|
142
142
|
setFocusedComponent("list");
|
|
143
143
|
focusNext();
|
|
144
144
|
} }) }), _jsxs(Box, { width: "60%", paddingX: 1, justifyContent: "space-between", children: [statusMessage ? (_jsx(Box, { padding: 1, flexShrink: 1, children: _jsx(Text, { color: statusMessageColor, children: statusMessage }) })) : (_jsx(Box, {})), selectedCipher && (_jsxs(Box, { gap: 1, flexShrink: 0, children: [_jsx(TabButton, { active: false, onClick: async () => {
|
|
@@ -38,7 +38,8 @@ export function VaultList({ filteredCiphers, isFocused, selected, onSelect, }) {
|
|
|
38
38
|
});
|
|
39
39
|
useInput((input, key) => {
|
|
40
40
|
const cipher = selected !== null ? filteredCiphers[selected] : null;
|
|
41
|
-
let field
|
|
41
|
+
let field;
|
|
42
|
+
let fldName;
|
|
42
43
|
if (key.ctrl && input === "y") {
|
|
43
44
|
switch (cipher?.type) {
|
|
44
45
|
case CipherType.Login:
|
|
@@ -74,7 +75,7 @@ export function VaultList({ filteredCiphers, isFocused, selected, onSelect, }) {
|
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
if (field) {
|
|
77
|
-
clipboard.
|
|
78
|
+
clipboard.write(field);
|
|
78
79
|
showStatusMessage(`📋 Copied ${fldName} to clipboard!`, "success");
|
|
79
80
|
}
|
|
80
81
|
}, { isActive: isFocused });
|
package/dist/debug.d.ts
ADDED
package/dist/debug.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
export const debugEnabled = process.argv.includes("--debug");
|
|
5
|
+
const logDir = path.join(os.homedir(), ".config", "bitty");
|
|
6
|
+
export const debugLogPath = path.join(logDir, "debug.log");
|
|
7
|
+
if (debugEnabled) {
|
|
8
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
9
|
+
fs.writeFileSync(debugLogPath, `--- debug session started ${new Date().toISOString()} ---\n`);
|
|
10
|
+
}
|
|
11
|
+
export function debugLog(message) {
|
|
12
|
+
if (!debugEnabled)
|
|
13
|
+
return;
|
|
14
|
+
fs.appendFileSync(debugLogPath, `[${new Date().toISOString()}] ${message}\n`);
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bitty-tui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": "https://github.com/mceck/bitty",
|
|
6
6
|
"keywords": [
|
|
@@ -32,18 +32,18 @@
|
|
|
32
32
|
"argon2": "^0.44.0",
|
|
33
33
|
"chalk": "^5.6.2",
|
|
34
34
|
"clipboardy": "^5.3.1",
|
|
35
|
-
"ink": "^6.
|
|
36
|
-
"otplib": "^13.
|
|
37
|
-
"react": "^19.
|
|
35
|
+
"ink": "^6.8.0",
|
|
36
|
+
"otplib": "^13.4.0",
|
|
37
|
+
"react": "^19.2.5",
|
|
38
38
|
"read-package-up": "^12.0.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@sindresorhus/tsconfig": "^8.0
|
|
42
|
-
"@types/node": "^24.
|
|
43
|
-
"@types/react": "^19.
|
|
44
|
-
"eslint-plugin-react": "^7.
|
|
41
|
+
"@sindresorhus/tsconfig": "^8.1.0",
|
|
42
|
+
"@types/node": "^24.12.2",
|
|
43
|
+
"@types/react": "^19.2.14",
|
|
44
|
+
"eslint-plugin-react": "^7.37.5",
|
|
45
45
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
46
46
|
"ink-testing-library": "^4.0.0",
|
|
47
|
-
"typescript": "^5.
|
|
47
|
+
"typescript": "^5.9.3"
|
|
48
48
|
}
|
|
49
49
|
}
|