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 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
  }
@@ -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, email, prelogin.kdfIterations);
123
+ masterKey = this.derivePbkdf2(password, normalizedEmail, prelogin.kdfIterations);
92
124
  }
93
125
  else {
94
- masterKey = await this.deriveArgon2(password, email, prelogin.kdfIterations, prelogin.kdfMemory, prelogin.kdfParallelism);
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
- this.apiUrl = uri.baseUrl + "/api";
347
- this.identityUrl = uri.baseUrl + "/identity";
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("username", email);
383
- bodyParams.append("password", keys.masterPasswordHash);
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("client_id", "web");
389
- bodyParams.append("scope", "api offline_access");
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
- accept: "*/*",
397
- "accept-language": "en-US",
398
- "bitwarden-client-name": "web",
399
- "bitwarden-client-version": "2025.9.0",
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
- "Content-Type": "application/x-www-form-urlencoded",
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: `refresh_token=${this.refreshToken}&grant_type=refresh_token&client_id=web&scope=api%20offline_access`,
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
  },
@@ -87,7 +87,7 @@ export const TextInput = ({ id, placeholder, value, isPassword, showPasswordOnFo
87
87
  onCopy(value);
88
88
  }
89
89
  else {
90
- clipboard.writeSync(value);
90
+ clipboard.write(value);
91
91
  showStatusMessage("📋 Copied to clipboard!", "success");
92
92
  }
93
93
  }
@@ -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, fldName;
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.writeSync(field);
78
+ clipboard.write(field);
78
79
  showStatusMessage(`📋 Copied ${fldName} to clipboard!`, "success");
79
80
  }
80
81
  }, { isActive: isFocused });
@@ -0,0 +1,3 @@
1
+ export declare const debugEnabled: boolean;
2
+ export declare const debugLogPath: string;
3
+ export declare function debugLog(message: string): void;
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.20",
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.3.0",
36
- "otplib": "^13.3.0",
37
- "react": "^19.1.0",
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.1",
42
- "@types/node": "^24.5.2",
43
- "@types/react": "^19.1.13",
44
- "eslint-plugin-react": "^7.32.2",
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.0.3"
47
+ "typescript": "^5.9.3"
48
48
  }
49
49
  }