bitty-tui 0.0.15 → 0.0.17

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.
@@ -17,7 +17,6 @@
17
17
  * - Cipher-specific key if available
18
18
  * 4. Decrypt cipher fields using the chosen key
19
19
  */
20
- import https from "node:https";
21
20
  import crypto from "node:crypto";
22
21
  import * as argon2 from "argon2";
23
22
  export class FetchError extends Error {
@@ -32,54 +31,14 @@ export class FetchError extends Error {
32
31
  return JSON.parse(this.data);
33
32
  }
34
33
  }
35
- function fetch(url, options = {}, body = null) {
36
- return new Promise((resolve, reject) => {
37
- const urlObj = new URL(url);
38
- const requestOptions = {
39
- hostname: urlObj.hostname,
40
- path: urlObj.pathname + urlObj.search,
41
- method: options.method || "GET",
42
- headers: options.headers || {},
43
- };
44
- const req = https.request(requestOptions, (res) => {
45
- let data = "";
46
- const onData = (chunk) => {
47
- data += chunk;
48
- };
49
- const onEnd = () => {
50
- cleanup();
51
- if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
52
- reject(new FetchError(res.statusCode, data, `HTTP error: ${res.statusCode} ${res.statusMessage}`));
53
- return;
54
- }
55
- resolve({
56
- status: res.statusCode,
57
- json: () => Promise.resolve(JSON.parse(data)),
58
- text: () => Promise.resolve(data),
59
- });
60
- };
61
- const onError = (error) => {
62
- cleanup();
63
- reject(error);
64
- };
65
- const cleanup = () => {
66
- res.removeListener("data", onData);
67
- res.removeListener("end", onEnd);
68
- res.removeListener("error", onError);
69
- };
70
- res.on("data", onData);
71
- res.on("end", onEnd);
72
- res.on("error", onError);
73
- });
74
- req.on("error", (error) => {
75
- reject(error);
76
- });
77
- if (body) {
78
- req.write(typeof body === "string" ? body : JSON.stringify(body));
79
- }
80
- req.end();
81
- });
82
- }
34
+ const fetchApi = async (...args) => {
35
+ const response = await fetch(...args);
36
+ if (!response.ok) {
37
+ const data = await response.text();
38
+ throw new FetchError(response.status, data);
39
+ }
40
+ return response;
41
+ };
83
42
  export var CipherType;
84
43
  (function (CipherType) {
85
44
  CipherType[CipherType["Login"] = 1] = "Login";
@@ -409,13 +368,12 @@ export class Client {
409
368
  async login(email, password, skipPrelogin = false, opts) {
410
369
  let keys = this.keys;
411
370
  if (!skipPrelogin) {
412
- const prelogin = await fetch(`${this.identityUrl}/accounts/prelogin`, {
371
+ const prelogin = await fetchApi(`${this.identityUrl}/accounts/prelogin`, {
413
372
  method: "POST",
414
373
  headers: {
415
374
  "Content-Type": "application/json",
416
375
  },
417
- }, {
418
- email,
376
+ body: JSON.stringify({ email }),
419
377
  }).then((r) => r.json());
420
378
  keys = await mcbw.deriveMasterKey(email, password, prelogin);
421
379
  this.keys = keys;
@@ -432,16 +390,22 @@ export class Client {
432
390
  for (const [key, value] of Object.entries(opts || {})) {
433
391
  bodyParams.append(key, value);
434
392
  }
435
- const identityReq = await fetch(`${this.identityUrl}/connect/token`, {
393
+ const identityReq = await fetchApi(`${this.identityUrl}/connect/token`, {
436
394
  method: "POST",
437
395
  headers: {
438
- "Content-Type": "application/x-www-form-urlencoded",
396
+ accept: "*/*",
397
+ "accept-language": "en-US",
398
+ "bitwarden-client-name": "web",
399
+ "bitwarden-client-version": "2025.9.0",
400
+ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
439
401
  },
440
- }, bodyParams.toString()).then((r) => r.json());
402
+ body: bodyParams.toString(),
403
+ }).then((r) => r.json());
441
404
  this.token = identityReq.access_token;
442
405
  this.refreshToken = identityReq.refresh_token;
443
406
  this.tokenExpiration = Date.now() + identityReq.expires_in * 1000;
444
407
  const { userKey, privateKey } = mcbw.decodeUserKeys(identityReq.Key, identityReq.PrivateKey || "", keys);
408
+ keys.masterKey = undefined; // Clear master key from memory
445
409
  this.keys = {
446
410
  ...keys,
447
411
  userKey,
@@ -452,19 +416,20 @@ export class Client {
452
416
  this.orgKeys = {};
453
417
  }
454
418
  async sendEmailMfaCode(email) {
455
- fetch(`${this.apiUrl}/two-factor/send-email-login`, {
419
+ fetchApi(`${this.apiUrl}/two-factor/send-email-login`, {
456
420
  method: "POST",
457
421
  headers: {
458
422
  "Content-Type": "application/json",
459
423
  },
460
- }, JSON.stringify({
461
- email: email,
462
- masterPasswordHash: this.keys.masterPasswordHash,
463
- ssoEmail2FaSessionToken: "",
464
- deviceIdentifier: DEVICE_IDENTIFIER,
465
- authRequestAccessCode: "",
466
- authRequestId: "",
467
- }));
424
+ body: JSON.stringify({
425
+ email: email,
426
+ masterPasswordHash: this.keys.masterPasswordHash,
427
+ ssoEmail2FaSessionToken: "",
428
+ deviceIdentifier: DEVICE_IDENTIFIER,
429
+ authRequestAccessCode: "",
430
+ authRequestId: "",
431
+ }),
432
+ });
468
433
  }
469
434
  // Check and refresh token if needed
470
435
  async checkToken() {
@@ -472,12 +437,13 @@ export class Client {
472
437
  if (!this.refreshToken) {
473
438
  throw new Error("No refresh token available. Please login first.");
474
439
  }
475
- const identityReq = await fetch(`${this.identityUrl}/connect/token`, {
440
+ const identityReq = await fetchApi(`${this.identityUrl}/connect/token`, {
476
441
  method: "POST",
477
442
  headers: {
478
443
  "Content-Type": "application/x-www-form-urlencoded",
479
444
  },
480
- }, `refresh_token=${this.refreshToken}&grant_type=refresh_token&client_id=web&scope=api%20offline_access`).then((r) => r.json());
445
+ body: `refresh_token=${this.refreshToken}&grant_type=refresh_token&client_id=web&scope=api%20offline_access`,
446
+ }).then((r) => r.json());
481
447
  this.token = identityReq.access_token;
482
448
  this.refreshToken = identityReq.refresh_token;
483
449
  this.tokenExpiration = Date.now() + identityReq.expires_in * 1000;
@@ -490,7 +456,7 @@ export class Client {
490
456
  */
491
457
  async syncRefresh() {
492
458
  await this.checkToken();
493
- this.syncCache = await fetch(`${this.apiUrl}/sync?excludeDomains=true`, {
459
+ this.syncCache = await fetchApi(`${this.apiUrl}/sync?excludeDomains=true`, {
494
460
  method: "GET",
495
461
  headers: {
496
462
  Authorization: `Bearer ${this.token}`,
@@ -636,13 +602,14 @@ export class Client {
636
602
  }
637
603
  async createSecret(obj) {
638
604
  const key = this.getDecryptionKey(obj);
639
- const s = await fetch(`${this.apiUrl}/ciphers`, {
605
+ const s = await fetchApi(`${this.apiUrl}/ciphers`, {
640
606
  method: "POST",
641
607
  headers: {
642
608
  Authorization: `Bearer ${this.token}`,
643
609
  "Content-Type": "application/json",
644
610
  },
645
- }, this.encryptCipher(obj, key));
611
+ body: JSON.stringify(this.encryptCipher(obj, key)),
612
+ });
646
613
  return s.json();
647
614
  }
648
615
  objectDiff(obj1, obj2) {
@@ -682,13 +649,14 @@ export class Client {
682
649
  const key = this.getDecryptionKey(patch);
683
650
  const data = this.patchObject(original, this.encryptCipher(obj, key));
684
651
  data.data = undefined;
685
- const s = await fetch(`${this.apiUrl}/ciphers/${id}`, {
652
+ const s = await fetchApi(`${this.apiUrl}/ciphers/${id}`, {
686
653
  method: "PUT",
687
654
  headers: {
688
655
  Authorization: `Bearer ${this.token}`,
689
656
  "Content-Type": "application/json",
690
657
  },
691
- }, data);
658
+ body: JSON.stringify(data),
659
+ });
692
660
  this.decryptedSyncCache = null;
693
661
  this.syncCache = null;
694
662
  return s.json();
@@ -5,6 +5,12 @@ interface BwConfig {
5
5
  refreshToken: string;
6
6
  }
7
7
  export declare const bwClient: Client;
8
+ export interface LoginHints {
9
+ email?: string;
10
+ baseUrl?: string;
11
+ }
12
+ export declare function loadLoginHints(): Promise<LoginHints>;
13
+ export declare function saveLoginHints(hints: LoginHints): Promise<void>;
8
14
  export declare function loadConfig(): Promise<boolean>;
9
15
  export declare function saveConfig(config: BwConfig): Promise<void>;
10
16
  export declare function clearConfig(): Promise<void>;
package/dist/hooks/bw.js CHANGED
@@ -4,7 +4,34 @@ import path from "path";
4
4
  import { CipherType, Client } from "../clients/bw.js";
5
5
  import { useCallback, useEffect, useState } from "react";
6
6
  export const bwClient = new Client();
7
- const configPath = path.join(os.homedir(), ".config", "bitty", "config.json");
7
+ const configDir = path.join(os.homedir(), ".config", "bitty");
8
+ const configPath = path.join(configDir, "config.json");
9
+ export async function loadLoginHints() {
10
+ try {
11
+ if (fs.existsSync(configPath)) {
12
+ const content = await fs.promises.readFile(configPath, "utf-8");
13
+ const config = JSON.parse(Buffer.from(content, "base64").toString("utf-8"));
14
+ return { email: config.email, baseUrl: config.baseUrl };
15
+ }
16
+ }
17
+ catch { }
18
+ return {};
19
+ }
20
+ export async function saveLoginHints(hints) {
21
+ let config = {};
22
+ try {
23
+ if (fs.existsSync(configPath)) {
24
+ const content = await fs.promises.readFile(configPath, "utf-8");
25
+ config = JSON.parse(Buffer.from(content, "base64").toString("utf-8"));
26
+ }
27
+ }
28
+ catch { }
29
+ config.email = hints.email;
30
+ config.baseUrl = hints.baseUrl;
31
+ const encoded = Buffer.from(JSON.stringify(config)).toString("base64");
32
+ await fs.promises.mkdir(configDir, { recursive: true });
33
+ await fs.promises.writeFile(configPath, encoded);
34
+ }
8
35
  export async function loadConfig() {
9
36
  try {
10
37
  if (fs.existsSync(configPath)) {
@@ -15,8 +42,6 @@ export async function loadConfig() {
15
42
  }
16
43
  if (config.keys && config.refreshToken) {
17
44
  const keys = {};
18
- if (config.keys.masterKey)
19
- keys.masterKey = Uint8Array.from(config.keys.masterKey);
20
45
  if (config.keys.masterPasswordHash)
21
46
  keys.masterPasswordHash = config.keys.masterPasswordHash;
22
47
  if (config.keys.privateKey)
@@ -50,8 +75,6 @@ export async function loadConfig() {
50
75
  }
51
76
  export async function saveConfig(config) {
52
77
  const keys = {};
53
- if (config.keys.masterKey)
54
- keys.masterKey = Array.from(config.keys.masterKey);
55
78
  if (config.keys.masterPasswordHash)
56
79
  keys.masterPasswordHash = config.keys.masterPasswordHash;
57
80
  if (config.keys.privateKey)
@@ -73,7 +96,7 @@ export async function saveConfig(config) {
73
96
  ...config,
74
97
  keys,
75
98
  })).toString("base64");
76
- await fs.promises.mkdir(path.dirname(configPath), { recursive: true });
99
+ await fs.promises.mkdir(configDir, { recursive: true });
77
100
  await fs.promises.writeFile(configPath, encConfig);
78
101
  }
79
102
  export async function clearConfig() {
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
4
4
  import { TextInput } from "../components/TextInput.js";
5
5
  import { Button } from "../components/Button.js";
6
6
  import { primary } from "../theme/style.js";
7
- import { bwClient, loadConfig, saveConfig } from "../hooks/bw.js";
7
+ import { bwClient, loadConfig, loadLoginHints, saveConfig, saveLoginHints } from "../hooks/bw.js";
8
8
  import { useStatusMessage } from "../hooks/status-message.js";
9
9
  import { Checkbox } from "../components/Checkbox.js";
10
10
  import { FetchError, TwoFactorProvider } from "../clients/bw.js";
@@ -94,6 +94,12 @@ export function LoginView({ onLogin }) {
94
94
  refreshToken: bwClient.refreshToken,
95
95
  });
96
96
  }
97
+ else {
98
+ saveLoginHints({
99
+ email: email?.trim() || undefined,
100
+ baseUrl: url?.trim() || undefined,
101
+ });
102
+ }
97
103
  }
98
104
  catch (e) {
99
105
  showStatusMessage("Login failed, please check your credentials.", "error");
@@ -105,7 +111,13 @@ export function LoginView({ onLogin }) {
105
111
  const loggedIn = await loadConfig();
106
112
  if (loggedIn) {
107
113
  onLogin();
114
+ return;
108
115
  }
116
+ const hints = await loadLoginHints();
117
+ if (hints.baseUrl)
118
+ setUrl(hints.baseUrl);
119
+ if (hints.email)
120
+ setEmail(hints.email);
109
121
  }
110
122
  catch (e) {
111
123
  showStatusMessage("Failed to load config file", "error");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitty-tui",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "license": "MIT",
5
5
  "repository": "https://github.com/mceck/bitty",
6
6
  "keywords": [
package/readme.md CHANGED
@@ -3,7 +3,13 @@
3
3
  ## Install
4
4
 
5
5
  ```bash
6
- $ npm install -g bitty-tui
6
+ npm install -g bitty-tui
7
+ ```
8
+
9
+ Run with:
10
+
11
+ ```bash
12
+ bitty
7
13
  ```
8
14
 
9
15
  ## Description
@@ -20,13 +26,13 @@ If you check "Remember me" during login, your vault encryption keys will be stor
20
26
  ## TODO
21
27
 
22
28
  - Collections support
23
- - Implement Fido, Duo MFA support
29
+ - Test Fido, Duo MFA support
24
30
  - Handle more fields editing
25
31
  - Handle creating different cipher types
26
32
 
27
33
  ## Acknowledgments
28
-
29
- - [Bitwarden](https://github.com/bitwarden)
34
+ - [Bitwarden whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper)
35
+ - [Bitwarden github](https://github.com/bitwarden)
30
36
  - [BitwardenDecrypt](https://github.com/GurpreetKang/BitwardenDecrypt)
31
37
 
32
38
  This project is not associated with [Bitwarden](https://github.com/bitwarden) or [Bitwarden, Inc.](https://bitwarden.com/)