bitty-tui 0.0.21 → 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.
@@ -373,6 +404,8 @@ export class Client {
373
404
  const prelogin = await fetchApi(`${this.identityUrl}/accounts/prelogin`, {
374
405
  method: "POST",
375
406
  headers: {
407
+ ...defaultHeaders(),
408
+ Accept: "application/json",
376
409
  "Content-Type": "application/json",
377
410
  },
378
411
  body: JSON.stringify({ email }),
@@ -381,24 +414,24 @@ export class Client {
381
414
  this.keys = keys;
382
415
  }
383
416
  const bodyParams = new URLSearchParams();
384
- bodyParams.append("username", email);
385
- bodyParams.append("password", keys.masterPasswordHash);
386
- bodyParams.append("grant_type", "password");
387
- bodyParams.append("deviceName", "chrome");
417
+ bodyParams.append("scope", "api offline_access");
418
+ bodyParams.append("client_id", "web");
388
419
  bodyParams.append("deviceType", "9");
389
420
  bodyParams.append("deviceIdentifier", DEVICE_IDENTIFIER);
390
- bodyParams.append("client_id", "web");
391
- 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);
392
425
  for (const [key, value] of Object.entries(opts || {})) {
393
426
  bodyParams.append(key, value);
394
427
  }
395
428
  const identityReq = await fetchApi(`${this.identityUrl}/connect/token`, {
396
429
  method: "POST",
397
430
  headers: {
398
- accept: "*/*",
399
- "accept-language": "en-US",
400
- "bitwarden-client-name": "web",
401
- "bitwarden-client-version": "2025.9.0",
431
+ ...defaultHeaders(),
432
+ Accept: "application/json",
433
+ "Cache-Control": "no-store",
434
+ Pragma: "no-cache",
402
435
  "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
403
436
  },
404
437
  body: bodyParams.toString(),
@@ -421,6 +454,8 @@ export class Client {
421
454
  return fetchApi(`${this.apiUrl}/two-factor/send-email-login`, {
422
455
  method: "POST",
423
456
  headers: {
457
+ ...defaultHeaders(),
458
+ Accept: "application/json",
424
459
  "Content-Type": "application/json",
425
460
  },
426
461
  body: JSON.stringify({
@@ -440,14 +475,17 @@ export class Client {
440
475
  throw new Error("No refresh token available. Please login first.");
441
476
  }
442
477
  const bodyParams = new URLSearchParams();
443
- bodyParams.append("refresh_token", this.refreshToken);
444
478
  bodyParams.append("grant_type", "refresh_token");
479
+ bodyParams.append("refresh_token", this.refreshToken);
445
480
  bodyParams.append("client_id", "web");
446
- bodyParams.append("scope", "api offline_access");
447
481
  const identityReq = await fetchApi(`${this.identityUrl}/connect/token`, {
448
482
  method: "POST",
449
483
  headers: {
450
- "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",
451
489
  },
452
490
  body: bodyParams.toString(),
453
491
  }).then((r) => r.json());
@@ -466,8 +504,9 @@ export class Client {
466
504
  this.syncCache = await fetchApi(`${this.apiUrl}/sync?excludeDomains=true`, {
467
505
  method: "GET",
468
506
  headers: {
507
+ ...defaultHeaders(),
508
+ Accept: "application/json",
469
509
  Authorization: `Bearer ${this.token}`,
470
- "bitwarden-client-version": "2025.9.0",
471
510
  },
472
511
  }).then((r) => r.json());
473
512
  this.decryptOrgKeys();
@@ -638,6 +677,8 @@ export class Client {
638
677
  const s = await fetchApi(url, {
639
678
  method: "POST",
640
679
  headers: {
680
+ // ...defaultHeaders(),
681
+ // Accept: "application/json",
641
682
  Authorization: `Bearer ${this.token}`,
642
683
  "Content-Type": "application/json",
643
684
  },
@@ -685,6 +726,8 @@ export class Client {
685
726
  const s = await fetchApi(`${this.apiUrl}/ciphers/${id}`, {
686
727
  method: "PUT",
687
728
  headers: {
729
+ // ...defaultHeaders(),
730
+ // Accept: "application/json",
688
731
  Authorization: `Bearer ${this.token}`,
689
732
  "Content-Type": "application/json",
690
733
  },
@@ -699,6 +742,8 @@ export class Client {
699
742
  await fetchApi(`${this.apiUrl}/ciphers/${id}`, {
700
743
  method: "DELETE",
701
744
  headers: {
745
+ // ...defaultHeaders(),
746
+ // Accept: "application/json",
702
747
  Authorization: `Bearer ${this.token}`,
703
748
  },
704
749
  });
@@ -722,6 +767,8 @@ export class Client {
722
767
  const s = await fetchApi(`${this.apiUrl}/ciphers/${id}/share`, {
723
768
  method: "PUT",
724
769
  headers: {
770
+ // ...defaultHeaders(),
771
+ // Accept: "application/json",
725
772
  Authorization: `Bearer ${this.token}`,
726
773
  "Content-Type": "application/json",
727
774
  },
@@ -739,6 +786,8 @@ export class Client {
739
786
  const s = await fetchApi(`${this.apiUrl}/ciphers/${id}/collections_v2`, {
740
787
  method: "PUT",
741
788
  headers: {
789
+ // ...defaultHeaders(),
790
+ // Accept: "application/json",
742
791
  Authorization: `Bearer ${this.token}`,
743
792
  "Content-Type": "application/json",
744
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.21",
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
  }