bitty-tui 0.0.18 → 0.0.19
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 +0 -0
- package/dist/clients/bw.d.ts +27 -1
- package/dist/clients/bw.js +107 -2
- package/dist/components/Button.d.ts +2 -1
- package/dist/components/Button.js +20 -3
- package/dist/components/TabButton.d.ts +9 -0
- package/dist/components/TabButton.js +11 -0
- package/dist/dashboard/DashboardView.js +56 -3
- package/dist/dashboard/components/CipherDetail.d.ts +9 -2
- package/dist/dashboard/components/CipherDetail.js +3 -5
- package/dist/dashboard/components/CollectionsTab.d.ts +7 -0
- package/dist/dashboard/components/CollectionsTab.js +48 -0
- package/dist/dashboard/components/MainInfoTab.js +4 -29
- package/dist/dashboard/components/MainTab.d.ts +4 -2
- package/dist/dashboard/components/MainTab.js +97 -58
- package/dist/dashboard/components/MoreInfoTab.d.ts +6 -1
- package/dist/dashboard/components/MoreInfoTab.js +79 -4
- package/dist/hooks/bw.d.ts +2 -1
- package/dist/hooks/bw.js +56 -11
- package/package.json +3 -3
- package/readme.md +0 -3
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/clients/bw.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export interface Cipher {
|
|
|
45
45
|
type: CipherType;
|
|
46
46
|
key?: string | null;
|
|
47
47
|
folderId?: string | null;
|
|
48
|
-
organizationId
|
|
48
|
+
organizationId?: string | null;
|
|
49
49
|
collectionIds?: string[] | null;
|
|
50
50
|
deletedDate: string | null;
|
|
51
51
|
name: string;
|
|
@@ -63,6 +63,14 @@ export interface Cipher {
|
|
|
63
63
|
totp?: string | null;
|
|
64
64
|
currentTotp?: string | null;
|
|
65
65
|
};
|
|
66
|
+
card?: {
|
|
67
|
+
cardholderName: string | null;
|
|
68
|
+
brand: string | null;
|
|
69
|
+
number: string | null;
|
|
70
|
+
expMonth: string | null;
|
|
71
|
+
expYear: string | null;
|
|
72
|
+
code: string | null;
|
|
73
|
+
};
|
|
66
74
|
identity?: {
|
|
67
75
|
address1: string | null;
|
|
68
76
|
address2: string | null;
|
|
@@ -94,6 +102,12 @@ export interface Cipher {
|
|
|
94
102
|
type: number;
|
|
95
103
|
}[];
|
|
96
104
|
}
|
|
105
|
+
export interface Collection {
|
|
106
|
+
id: string;
|
|
107
|
+
organizationId: string;
|
|
108
|
+
name: string;
|
|
109
|
+
readOnly: boolean;
|
|
110
|
+
}
|
|
97
111
|
export type CipherDto = Omit<Cipher, "id" | "data">;
|
|
98
112
|
export declare enum KdfType {
|
|
99
113
|
PBKDF2 = 0,
|
|
@@ -101,6 +115,7 @@ export declare enum KdfType {
|
|
|
101
115
|
}
|
|
102
116
|
export interface SyncResponse {
|
|
103
117
|
ciphers: Cipher[];
|
|
118
|
+
collections?: Collection[];
|
|
104
119
|
profile?: {
|
|
105
120
|
organizations?: {
|
|
106
121
|
id: string;
|
|
@@ -184,6 +199,9 @@ export declare class Client {
|
|
|
184
199
|
createSecret(obj: CipherDto): Promise<any>;
|
|
185
200
|
objectDiff(obj1: any, obj2: any): any;
|
|
186
201
|
updateSecret(id: string, patch: Partial<CipherDto>): Promise<any>;
|
|
202
|
+
deleteSecret(id: string): Promise<void>;
|
|
203
|
+
shareCipher(id: string, cipher: Partial<CipherDto>, collectionIds: string[]): Promise<any>;
|
|
204
|
+
updateCollections(id: string, collectionIds: string[]): Promise<any>;
|
|
187
205
|
encrypt(value: string | null, key?: any): string;
|
|
188
206
|
decrypt(value: string | null | undefined, key?: any): string | null | undefined;
|
|
189
207
|
encryptCipher(obj: Partial<CipherDto>, key?: any): {
|
|
@@ -208,6 +226,14 @@ export declare class Client {
|
|
|
208
226
|
totp?: string | null;
|
|
209
227
|
currentTotp?: string | null;
|
|
210
228
|
} | undefined;
|
|
229
|
+
card?: {
|
|
230
|
+
cardholderName: string | null;
|
|
231
|
+
brand: string | null;
|
|
232
|
+
number: string | null;
|
|
233
|
+
expMonth: string | null;
|
|
234
|
+
expYear: string | null;
|
|
235
|
+
code: string | null;
|
|
236
|
+
} | undefined;
|
|
211
237
|
identity?: {
|
|
212
238
|
address1: string | null;
|
|
213
239
|
address2: string | null;
|
package/dist/clients/bw.js
CHANGED
|
@@ -504,6 +504,13 @@ export class Client {
|
|
|
504
504
|
}
|
|
505
505
|
this.decryptedSyncCache = {
|
|
506
506
|
...this.syncCache,
|
|
507
|
+
collections: this.syncCache.collections?.map((col) => {
|
|
508
|
+
const orgKey = this.orgKeys[col.organizationId];
|
|
509
|
+
return {
|
|
510
|
+
...col,
|
|
511
|
+
name: orgKey ? this.decrypt(col.name, orgKey) ?? col.name : col.name,
|
|
512
|
+
};
|
|
513
|
+
}),
|
|
507
514
|
ciphers: this.syncCache.ciphers.map((cipher) => {
|
|
508
515
|
const key = this.getDecryptionKey(cipher);
|
|
509
516
|
const ret = JSON.parse(JSON.stringify(cipher));
|
|
@@ -555,6 +562,17 @@ export class Client {
|
|
|
555
562
|
this.decrypt(cipher.identity.username, key),
|
|
556
563
|
};
|
|
557
564
|
}
|
|
565
|
+
if (cipher.card) {
|
|
566
|
+
ret.card = {
|
|
567
|
+
cardholderName: cipher.card.cardholderName &&
|
|
568
|
+
this.decrypt(cipher.card.cardholderName, key),
|
|
569
|
+
brand: cipher.card.brand && this.decrypt(cipher.card.brand, key),
|
|
570
|
+
number: cipher.card.number && this.decrypt(cipher.card.number, key),
|
|
571
|
+
expMonth: cipher.card.expMonth && this.decrypt(cipher.card.expMonth, key),
|
|
572
|
+
expYear: cipher.card.expYear && this.decrypt(cipher.card.expYear, key),
|
|
573
|
+
code: cipher.card.code && this.decrypt(cipher.card.code, key),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
558
576
|
if (cipher.sshKey) {
|
|
559
577
|
ret.sshKey = {
|
|
560
578
|
keyFingerprint: cipher.sshKey.keyFingerprint &&
|
|
@@ -602,13 +620,21 @@ export class Client {
|
|
|
602
620
|
}
|
|
603
621
|
async createSecret(obj) {
|
|
604
622
|
const key = this.getDecryptionKey(obj);
|
|
605
|
-
const
|
|
623
|
+
const encrypted = this.encryptCipher(obj, key);
|
|
624
|
+
const hasCollections = obj.collectionIds?.length;
|
|
625
|
+
const url = hasCollections
|
|
626
|
+
? `${this.apiUrl}/ciphers/create`
|
|
627
|
+
: `${this.apiUrl}/ciphers`;
|
|
628
|
+
const body = hasCollections
|
|
629
|
+
? { cipher: encrypted, collectionIds: obj.collectionIds }
|
|
630
|
+
: encrypted;
|
|
631
|
+
const s = await fetchApi(url, {
|
|
606
632
|
method: "POST",
|
|
607
633
|
headers: {
|
|
608
634
|
Authorization: `Bearer ${this.token}`,
|
|
609
635
|
"Content-Type": "application/json",
|
|
610
636
|
},
|
|
611
|
-
body: JSON.stringify(
|
|
637
|
+
body: JSON.stringify(body),
|
|
612
638
|
});
|
|
613
639
|
return s.json();
|
|
614
640
|
}
|
|
@@ -661,6 +687,60 @@ export class Client {
|
|
|
661
687
|
this.syncCache = null;
|
|
662
688
|
return s.json();
|
|
663
689
|
}
|
|
690
|
+
async deleteSecret(id) {
|
|
691
|
+
await this.checkToken();
|
|
692
|
+
await fetchApi(`${this.apiUrl}/ciphers/${id}`, {
|
|
693
|
+
method: "DELETE",
|
|
694
|
+
headers: {
|
|
695
|
+
Authorization: `Bearer ${this.token}`,
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
this.decryptedSyncCache = null;
|
|
699
|
+
this.syncCache = null;
|
|
700
|
+
}
|
|
701
|
+
async shareCipher(id, cipher, collectionIds) {
|
|
702
|
+
if (!collectionIds.length) {
|
|
703
|
+
throw new Error("At least one collection is required to share a cipher");
|
|
704
|
+
}
|
|
705
|
+
await this.getDecryptedSync();
|
|
706
|
+
const original = this.syncCache?.ciphers.find((c) => c.id === id);
|
|
707
|
+
if (!original) {
|
|
708
|
+
throw new Error("Secret not found in cache. Please sync first.");
|
|
709
|
+
}
|
|
710
|
+
const key = this.getDecryptionKey(cipher);
|
|
711
|
+
const encrypted = this.encryptCipher(cipher, key);
|
|
712
|
+
const data = this.patchObject(original, encrypted);
|
|
713
|
+
data.data = undefined;
|
|
714
|
+
await this.checkToken();
|
|
715
|
+
const s = await fetchApi(`${this.apiUrl}/ciphers/${id}/share`, {
|
|
716
|
+
method: "PUT",
|
|
717
|
+
headers: {
|
|
718
|
+
Authorization: `Bearer ${this.token}`,
|
|
719
|
+
"Content-Type": "application/json",
|
|
720
|
+
},
|
|
721
|
+
body: JSON.stringify({ cipher: data, collectionIds }),
|
|
722
|
+
});
|
|
723
|
+
this.decryptedSyncCache = null;
|
|
724
|
+
this.syncCache = null;
|
|
725
|
+
return s.json();
|
|
726
|
+
}
|
|
727
|
+
async updateCollections(id, collectionIds) {
|
|
728
|
+
if (!collectionIds.length) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
await this.checkToken();
|
|
732
|
+
const s = await fetchApi(`${this.apiUrl}/ciphers/${id}/collections_v2`, {
|
|
733
|
+
method: "PUT",
|
|
734
|
+
headers: {
|
|
735
|
+
Authorization: `Bearer ${this.token}`,
|
|
736
|
+
"Content-Type": "application/json",
|
|
737
|
+
},
|
|
738
|
+
body: JSON.stringify({ collectionIds }),
|
|
739
|
+
});
|
|
740
|
+
this.decryptedSyncCache = null;
|
|
741
|
+
this.syncCache = null;
|
|
742
|
+
return s.json();
|
|
743
|
+
}
|
|
664
744
|
encrypt(value, key) {
|
|
665
745
|
if (!value)
|
|
666
746
|
return value;
|
|
@@ -752,6 +832,28 @@ export class Client {
|
|
|
752
832
|
: ret.identity.username,
|
|
753
833
|
};
|
|
754
834
|
}
|
|
835
|
+
if (ret.card) {
|
|
836
|
+
ret.card = {
|
|
837
|
+
cardholderName: ret.card.cardholderName
|
|
838
|
+
? this.encrypt(ret.card.cardholderName, key)
|
|
839
|
+
: ret.card.cardholderName,
|
|
840
|
+
brand: ret.card.brand
|
|
841
|
+
? this.encrypt(ret.card.brand, key)
|
|
842
|
+
: ret.card.brand,
|
|
843
|
+
number: ret.card.number
|
|
844
|
+
? this.encrypt(ret.card.number, key)
|
|
845
|
+
: ret.card.number,
|
|
846
|
+
expMonth: ret.card.expMonth
|
|
847
|
+
? this.encrypt(ret.card.expMonth, key)
|
|
848
|
+
: ret.card.expMonth,
|
|
849
|
+
expYear: ret.card.expYear
|
|
850
|
+
? this.encrypt(ret.card.expYear, key)
|
|
851
|
+
: ret.card.expYear,
|
|
852
|
+
code: ret.card.code
|
|
853
|
+
? this.encrypt(ret.card.code, key)
|
|
854
|
+
: ret.card.code,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
755
857
|
if (ret.login) {
|
|
756
858
|
ret.login = {
|
|
757
859
|
...ret.login,
|
|
@@ -761,6 +863,9 @@ export class Client {
|
|
|
761
863
|
password: ret.login.password
|
|
762
864
|
? this.encrypt(ret.login.password, key)
|
|
763
865
|
: ret.login.password,
|
|
866
|
+
totp: ret.login.totp
|
|
867
|
+
? this.encrypt(ret.login.totp, key)
|
|
868
|
+
: ret.login.totp,
|
|
764
869
|
uri: ret.login.uri ? this.encrypt(ret.login.uri, key) : ret.login.uri,
|
|
765
870
|
uris: ret.login.uris?.map((uri) => ({
|
|
766
871
|
uri: uri.uri ? this.encrypt(uri.uri, key) : uri.uri,
|
|
@@ -3,9 +3,10 @@ import { ReactNode } from "react";
|
|
|
3
3
|
type Props = {
|
|
4
4
|
isActive?: boolean;
|
|
5
5
|
doubleConfirm?: boolean;
|
|
6
|
+
tripleConfirm?: boolean;
|
|
6
7
|
autoFocus?: boolean;
|
|
7
8
|
onClick: () => void;
|
|
8
9
|
children: ReactNode;
|
|
9
10
|
} & React.ComponentProps<typeof Box>;
|
|
10
|
-
export declare const Button: ({ isActive, doubleConfirm, onClick, children, autoFocus, ...props }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export declare const Button: ({ isActive, doubleConfirm, tripleConfirm, onClick, children, autoFocus, ...props }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
11
12
|
export {};
|
|
@@ -3,22 +3,33 @@ import { Text, Box, useFocus, useInput } from "ink";
|
|
|
3
3
|
import { useId, useRef, useState } from "react";
|
|
4
4
|
import { primary } from "../theme/style.js";
|
|
5
5
|
import { useMouseTarget } from "../hooks/use-mouse.js";
|
|
6
|
-
export const Button = ({ isActive = true, doubleConfirm, onClick, children, autoFocus = false, ...props }) => {
|
|
6
|
+
export const Button = ({ isActive = true, doubleConfirm, tripleConfirm, onClick, children, autoFocus = false, ...props }) => {
|
|
7
7
|
const generatedId = useId();
|
|
8
8
|
const { isFocused } = useFocus({ id: generatedId, autoFocus: autoFocus });
|
|
9
9
|
const [askConfirm, setAskConfirm] = useState(false);
|
|
10
|
+
const [ask2Confirm, setAsk2Confirm] = useState(false);
|
|
10
11
|
const timeoutRef = useRef(null);
|
|
11
12
|
const boxRef = useRef(null);
|
|
12
13
|
const handlePress = () => {
|
|
13
14
|
if (timeoutRef.current)
|
|
14
15
|
clearTimeout(timeoutRef.current);
|
|
15
|
-
if (doubleConfirm && !askConfirm) {
|
|
16
|
+
if ((doubleConfirm || tripleConfirm) && !askConfirm) {
|
|
16
17
|
setAskConfirm(true);
|
|
17
18
|
timeoutRef.current = setTimeout(() => setAskConfirm(false), 1000);
|
|
18
19
|
return;
|
|
19
20
|
}
|
|
21
|
+
if (tripleConfirm && !ask2Confirm) {
|
|
22
|
+
setAsk2Confirm(true);
|
|
23
|
+
timeoutRef.current = setTimeout(() => {
|
|
24
|
+
setAskConfirm(false);
|
|
25
|
+
setAsk2Confirm(false);
|
|
26
|
+
}, 1000);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
20
29
|
if (askConfirm)
|
|
21
30
|
setAskConfirm(false);
|
|
31
|
+
if (ask2Confirm)
|
|
32
|
+
setAsk2Confirm(false);
|
|
22
33
|
onClick();
|
|
23
34
|
};
|
|
24
35
|
useMouseTarget(generatedId, boxRef, { onClick: handlePress });
|
|
@@ -26,5 +37,11 @@ export const Button = ({ isActive = true, doubleConfirm, onClick, children, auto
|
|
|
26
37
|
if (key.return)
|
|
27
38
|
handlePress();
|
|
28
39
|
}, { isActive: isFocused && isActive });
|
|
29
|
-
return (_jsx(Box, { ref: boxRef, borderStyle: "round", borderColor: isFocused && isActive ? primary : "gray", alignItems: "center", justifyContent: "center", ...props, children: _jsx(Text, { color: isFocused && isActive
|
|
40
|
+
return (_jsx(Box, { ref: boxRef, borderStyle: "round", borderColor: isFocused && isActive ? primary : "gray", alignItems: "center", justifyContent: "center", ...props, children: _jsx(Text, { color: isFocused && isActive
|
|
41
|
+
? ask2Confirm
|
|
42
|
+
? "red"
|
|
43
|
+
: askConfirm
|
|
44
|
+
? "yellow"
|
|
45
|
+
: "white"
|
|
46
|
+
: "gray", children: ask2Confirm ? "Are you sure?" : askConfirm ? "Confirm?" : children }) }));
|
|
30
47
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
type Props = {
|
|
3
|
+
active?: boolean;
|
|
4
|
+
onClick: () => void;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
borderLess?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare const TabButton: ({ active, onClick, children, borderLess }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text, Box } from "ink";
|
|
3
|
+
import { useId, useRef } from "react";
|
|
4
|
+
import { primary } from "../theme/style.js";
|
|
5
|
+
import { useMouseTarget } from "../hooks/use-mouse.js";
|
|
6
|
+
export const TabButton = ({ active, onClick, children, borderLess }) => {
|
|
7
|
+
const id = useId();
|
|
8
|
+
const boxRef = useRef(null);
|
|
9
|
+
useMouseTarget(id, boxRef, { onClick });
|
|
10
|
+
return (_jsx(Box, { ref: boxRef, borderStyle: borderLess ? undefined : "round", borderColor: active ? primary : "gray", alignItems: "center", justifyContent: "center", paddingX: 1, children: _jsx(Text, { color: active ? "white" : "gray", children: children }) }));
|
|
11
|
+
};
|
|
@@ -6,9 +6,10 @@ import { VaultList } from "./components/VaultList.js";
|
|
|
6
6
|
import { CipherDetail } from "./components/CipherDetail.js";
|
|
7
7
|
import { HelpBar } from "./components/HelpBar.js";
|
|
8
8
|
import { primary } from "../theme/style.js";
|
|
9
|
-
import { bwClient, clearConfig, emptyCipher, useBwSync } from "../hooks/bw.js";
|
|
9
|
+
import { bwClient, clearConfig, createEmptyCipher, emptyCipher, useBwSync } from "../hooks/bw.js";
|
|
10
10
|
import { useStatusMessage } from "../hooks/status-message.js";
|
|
11
11
|
import { useMouseSubscribe } from "../hooks/use-mouse.js";
|
|
12
|
+
import { TabButton } from "../components/TabButton.js";
|
|
12
13
|
export function DashboardView({ onLogout }) {
|
|
13
14
|
const { sync, error, fetchSync } = useBwSync();
|
|
14
15
|
const [syncState, setSyncState] = useState(sync);
|
|
@@ -18,6 +19,7 @@ export function DashboardView({ onLogout }) {
|
|
|
18
19
|
const [focusedComponent, setFocusedComponent] = useState("list");
|
|
19
20
|
const [detailMode, setDetailMode] = useState("view");
|
|
20
21
|
const [editedCipher, setEditedCipher] = useState(null);
|
|
22
|
+
const [activeTab, setActiveTab] = useState("main");
|
|
21
23
|
const { focus, focusNext } = useFocusManager();
|
|
22
24
|
const { stdout } = useStdout();
|
|
23
25
|
const { statusMessage, statusMessageColor, showStatusMessage } = useStatusMessage();
|
|
@@ -42,6 +44,8 @@ export function DashboardView({ onLogout }) {
|
|
|
42
44
|
return i < 0 ? 0 : i;
|
|
43
45
|
}, [listSelected, filteredCiphers]);
|
|
44
46
|
const selectedCipher = detailMode === "new" ? editedCipher : filteredCiphers[listIndex];
|
|
47
|
+
const writableCollections = useMemo(() => (syncState?.collections ?? []).filter((c) => !c.readOnly && c.organizationId === selectedCipher?.organizationId), [syncState, editedCipher, filteredCiphers[listIndex]]);
|
|
48
|
+
const organizations = useMemo(() => (syncState?.profile?.organizations ?? []).map(({ id, name }) => ({ id, name })), [syncState]);
|
|
45
49
|
const logout = async () => {
|
|
46
50
|
bwClient.logout();
|
|
47
51
|
await clearConfig();
|
|
@@ -79,12 +83,14 @@ export function DashboardView({ onLogout }) {
|
|
|
79
83
|
setDetailMode("new");
|
|
80
84
|
setEditedCipher(emptyCipher);
|
|
81
85
|
setFocusedComponent("detail");
|
|
86
|
+
setActiveTab("main");
|
|
82
87
|
setShowDetails(false);
|
|
83
88
|
return;
|
|
84
89
|
}
|
|
85
90
|
if (key.escape) {
|
|
86
91
|
setFocusedComponent("list");
|
|
87
92
|
setDetailMode("view");
|
|
93
|
+
setActiveTab("main");
|
|
88
94
|
}
|
|
89
95
|
if (focusedComponent === "search") {
|
|
90
96
|
if (key.escape && searchQuery?.length) {
|
|
@@ -112,22 +118,46 @@ export function DashboardView({ onLogout }) {
|
|
|
112
118
|
return (_jsxs(Box, { flexDirection: "column", width: "100%", height: stdout.rows - 1, 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: () => {
|
|
113
119
|
setFocusedComponent("list");
|
|
114
120
|
focusNext();
|
|
115
|
-
} }) }),
|
|
121
|
+
} }) }), _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: activeTab === "main", onClick: () => setActiveTab("main"), children: "Main" }), _jsx(TabButton, { active: activeTab === "more", onClick: () => setActiveTab("more"), children: "More" }), !!syncState?.collections?.length && (_jsx(TabButton, { active: activeTab === "collections", onClick: () => setActiveTab("collections"), children: "Collections" }))] }))] })] }), _jsxs(Box, { minHeight: 20, flexGrow: 1, children: [_jsx(VaultList, { filteredCiphers: filteredCiphers, isFocused: ["list", "search"].includes(focusedComponent), selected: listIndex, onSelect: (index) => setListSelected(filteredCiphers[index] || null) }), _jsx(CipherDetail, { selectedCipher: showDetails ? selectedCipher : null, mode: detailMode, activeTab: activeTab, collections: writableCollections, organizations: organizations, isFocused: focusedComponent === "detail", onTypeChange: (type) => {
|
|
122
|
+
const fresh = createEmptyCipher(type);
|
|
123
|
+
fresh.name = editedCipher?.name ?? "";
|
|
124
|
+
fresh.notes = editedCipher?.notes ?? null;
|
|
125
|
+
fresh.collectionIds = editedCipher?.collectionIds ?? [];
|
|
126
|
+
fresh.organizationId = editedCipher?.organizationId ?? null;
|
|
127
|
+
setEditedCipher(fresh);
|
|
128
|
+
}, onChange: (cipher) => {
|
|
116
129
|
if (detailMode === "new") {
|
|
117
130
|
setEditedCipher(cipher);
|
|
118
131
|
return;
|
|
119
132
|
}
|
|
120
133
|
const updatedCiphers = syncState?.ciphers.map((c) => c.id === cipher.id ? cipher : c);
|
|
121
134
|
setSyncState((prev) => ({ ...prev, ciphers: updatedCiphers }));
|
|
135
|
+
}, onDelete: async (cipher) => {
|
|
136
|
+
showStatusMessage("Deleting...");
|
|
137
|
+
try {
|
|
138
|
+
await bwClient.deleteSecret(cipher.id);
|
|
139
|
+
fetchSync();
|
|
140
|
+
showStatusMessage("Deleted!", "success");
|
|
141
|
+
setFocusedComponent("list");
|
|
142
|
+
setActiveTab("main");
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
showStatusMessage("Delete error", "error");
|
|
146
|
+
}
|
|
122
147
|
}, onSave: async (cipher) => {
|
|
123
148
|
showStatusMessage("Saving...");
|
|
124
149
|
if (detailMode === "new") {
|
|
125
150
|
try {
|
|
151
|
+
if (cipher.organizationId && !cipher.collectionIds?.length) {
|
|
152
|
+
showStatusMessage("Select at least one collection", "error");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
126
155
|
await bwClient.createSecret(cipher);
|
|
127
156
|
fetchSync();
|
|
128
157
|
showStatusMessage("Saved!", "success");
|
|
129
158
|
setDetailMode("view");
|
|
130
159
|
setFocusedComponent("list");
|
|
160
|
+
setActiveTab("main");
|
|
131
161
|
}
|
|
132
162
|
catch (e) {
|
|
133
163
|
showStatusMessage("Synchronization error", "error");
|
|
@@ -135,14 +165,37 @@ export function DashboardView({ onLogout }) {
|
|
|
135
165
|
}
|
|
136
166
|
else {
|
|
137
167
|
try {
|
|
168
|
+
const original = sync?.ciphers.find((c) => c.id === cipher.id);
|
|
169
|
+
const originalOrgId = original?.organizationId ?? null;
|
|
170
|
+
const originalCollections = original?.collectionIds ?? [];
|
|
171
|
+
const newCollections = [...cipher.collectionIds ?? []];
|
|
172
|
+
// Sharing: personal cipher being assigned to an org
|
|
173
|
+
if (!originalOrgId && cipher.organizationId) {
|
|
174
|
+
if (!newCollections.length) {
|
|
175
|
+
showStatusMessage("Select at least one collection to share", "error");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
await bwClient.shareCipher(cipher.id, cipher, newCollections);
|
|
179
|
+
fetchSync();
|
|
180
|
+
showStatusMessage("Shared!", "success");
|
|
181
|
+
setFocusedComponent("list");
|
|
182
|
+
setActiveTab("main");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
138
185
|
const updated = await bwClient.updateSecret(cipher.id, cipher);
|
|
139
|
-
|
|
186
|
+
const collectionsChanged = originalCollections.length !== newCollections.length ||
|
|
187
|
+
originalCollections.some((id) => !newCollections.includes(id));
|
|
188
|
+
if (collectionsChanged) {
|
|
189
|
+
await bwClient.updateCollections(cipher.id, newCollections);
|
|
190
|
+
}
|
|
191
|
+
if (!updated && !collectionsChanged) {
|
|
140
192
|
showStatusMessage("Nothing to save");
|
|
141
193
|
return;
|
|
142
194
|
}
|
|
143
195
|
fetchSync();
|
|
144
196
|
showStatusMessage("Saved!", "success");
|
|
145
197
|
setFocusedComponent("list");
|
|
198
|
+
setActiveTab("main");
|
|
146
199
|
}
|
|
147
200
|
catch (e) {
|
|
148
201
|
showStatusMessage("Synchronization error", "error");
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { Cipher } from "../../clients/bw.js";
|
|
2
|
-
|
|
1
|
+
import { Cipher, CipherType, Collection } from "../../clients/bw.js";
|
|
2
|
+
import { Organization } from "./MoreInfoTab.js";
|
|
3
|
+
export type DetailTab = "main" | "more" | "collections";
|
|
4
|
+
export declare function CipherDetail({ selectedCipher, isFocused, mode, activeTab, collections, organizations, onChange, onSave, onDelete, onTypeChange, }: {
|
|
3
5
|
selectedCipher: Cipher | null | undefined;
|
|
4
6
|
isFocused: boolean;
|
|
5
7
|
mode: "view" | "new";
|
|
8
|
+
activeTab: DetailTab;
|
|
9
|
+
collections: Collection[];
|
|
10
|
+
organizations: Organization[];
|
|
6
11
|
onChange: (cipher: Cipher) => void;
|
|
7
12
|
onSave: (cipher: Cipher) => void;
|
|
13
|
+
onDelete: (cipher: Cipher) => void;
|
|
14
|
+
onTypeChange?: (type: CipherType) => void;
|
|
8
15
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box } from "ink";
|
|
3
3
|
import { primaryLight } from "../../theme/style.js";
|
|
4
|
-
import { CipherType } from "../../clients/bw.js";
|
|
5
4
|
import { Button } from "../../components/Button.js";
|
|
6
|
-
import { useState } from "react";
|
|
7
5
|
import { MoreInfoTab } from "./MoreInfoTab.js";
|
|
8
6
|
import { MainTab } from "./MainTab.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return (_jsx(Box, { flexDirection: "column", width: "60%", flexGrow: 1, paddingX: 1, borderStyle: "round", borderColor: isFocused ? primaryLight : "gray", borderLeftColor: "gray", children: selectedCipher && (_jsxs(Box, { flexDirection: "column", justifyContent: "space-between", flexGrow: 1, children: [
|
|
7
|
+
import { CollectionsTab } from "./CollectionsTab.js";
|
|
8
|
+
export function CipherDetail({ selectedCipher, isFocused, mode, activeTab, collections, organizations, onChange, onSave, onDelete, onTypeChange, }) {
|
|
9
|
+
return (_jsx(Box, { flexDirection: "column", width: "60%", flexGrow: 1, paddingX: 1, borderStyle: "round", borderColor: isFocused ? primaryLight : "gray", borderLeftColor: "gray", children: selectedCipher && (_jsxs(Box, { flexDirection: "column", justifyContent: "space-between", flexGrow: 1, children: [activeTab === "more" ? (_jsx(MoreInfoTab, { isFocused: isFocused, selectedCipher: selectedCipher, organizations: organizations, onChange: onChange })) : activeTab === "collections" ? (_jsx(CollectionsTab, { isFocused: isFocused, selectedCipher: selectedCipher, collections: collections, onChange: onChange })) : (_jsx(MainTab, { isFocused: isFocused, selectedCipher: selectedCipher, mode: mode, onChange: onChange, onTypeChange: onTypeChange })), _jsxs(Box, { marginTop: 1, flexShrink: 0, gap: 1, children: [_jsx(Button, { doubleConfirm: true, width: "50%", isActive: isFocused, onClick: () => onSave(selectedCipher), children: "Save" }), mode !== "new" && (_jsx(Button, { tripleConfirm: true, width: "50%", isActive: isFocused, onClick: () => onDelete(selectedCipher), children: "Delete" }))] })] })) }));
|
|
12
10
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Cipher, Collection } from "../../clients/bw.js";
|
|
2
|
+
export declare function CollectionsTab({ isFocused, selectedCipher, collections, onChange, }: {
|
|
3
|
+
isFocused: boolean;
|
|
4
|
+
selectedCipher: Cipher;
|
|
5
|
+
collections: Collection[];
|
|
6
|
+
onChange: (cipher: Cipher) => void;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
3
|
+
import { useId, useRef, useState } from "react";
|
|
4
|
+
import { useMouseTarget } from "../../hooks/use-mouse.js";
|
|
5
|
+
export function CollectionsTab({ isFocused, selectedCipher, collections, onChange, }) {
|
|
6
|
+
const { stdout } = useStdout();
|
|
7
|
+
const [cursor, setCursor] = useState(0);
|
|
8
|
+
const selected = selectedCipher.collectionIds ?? [];
|
|
9
|
+
useInput((_input, key) => {
|
|
10
|
+
if (key.upArrow) {
|
|
11
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
12
|
+
}
|
|
13
|
+
else if (key.downArrow) {
|
|
14
|
+
setCursor((c) => Math.min(collections.length - 1, c + 1));
|
|
15
|
+
}
|
|
16
|
+
}, { isActive: isFocused });
|
|
17
|
+
if (!collections.length) {
|
|
18
|
+
return (_jsx(Box, { flexDirection: "column", height: stdout.rows - 18, children: _jsx(Text, { color: "gray", children: "No writable collections available." }) }));
|
|
19
|
+
}
|
|
20
|
+
return (_jsx(Box, { flexDirection: "column", gap: 0, height: stdout.rows - 18, children: collections.map((col, idx) => {
|
|
21
|
+
const checked = selected.includes(col.id);
|
|
22
|
+
const isCursor = cursor === idx && isFocused;
|
|
23
|
+
return (_jsx(CollectionCheckbox, { col: col, isCursor: isCursor, checked: checked, onFocus: () => setCursor(idx), onChange: (checked) => onChange({
|
|
24
|
+
...selectedCipher,
|
|
25
|
+
collectionIds: checked
|
|
26
|
+
? [...selected, col.id]
|
|
27
|
+
: selected.filter((id) => id !== col.id),
|
|
28
|
+
}) }, col.id));
|
|
29
|
+
}) }));
|
|
30
|
+
function CollectionCheckbox({ col, isCursor, checked, onChange, onFocus, }) {
|
|
31
|
+
const checkRef = useRef(null);
|
|
32
|
+
const labelRef = useRef(null);
|
|
33
|
+
const checkId = useId();
|
|
34
|
+
const labelId = useId();
|
|
35
|
+
useMouseTarget(checkId, checkRef, {
|
|
36
|
+
onClick: () => onChange?.(!checked),
|
|
37
|
+
});
|
|
38
|
+
useMouseTarget(labelId, labelRef, {
|
|
39
|
+
onClick: () => onFocus?.(),
|
|
40
|
+
});
|
|
41
|
+
useInput((input) => {
|
|
42
|
+
if (input === " ") {
|
|
43
|
+
onChange?.(!checked);
|
|
44
|
+
}
|
|
45
|
+
}, { isActive: isCursor });
|
|
46
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { ref: checkRef, children: _jsx(Text, { color: isCursor ? "white" : "gray", bold: isCursor, children: checked ? "[x] " : "[ ] " }) }), _jsx(Box, { ref: labelRef, children: _jsx(Text, { color: isCursor ? "white" : "gray", children: col.name }) })] }, col.id));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -3,39 +3,14 @@ import { Box, Text } from "ink";
|
|
|
3
3
|
import { CipherType } from "../../clients/bw.js";
|
|
4
4
|
import { primaryLight } from "../../theme/style.js";
|
|
5
5
|
import { TextInput } from "../../components/TextInput.js";
|
|
6
|
-
import { useEffect, useState } from "react";
|
|
7
|
-
import { authenticator } from "otplib";
|
|
8
6
|
export function MainTab({ isFocused, selectedCipher, onChange, }) {
|
|
9
|
-
const [otpCode, setOtpCode] = useState("");
|
|
10
|
-
const [otpTimeout, setOtpTimeout] = useState(0);
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
let interval = null;
|
|
13
|
-
if (selectedCipher?.login?.totp) {
|
|
14
|
-
interval = setInterval(() => {
|
|
15
|
-
setOtpTimeout((t) => {
|
|
16
|
-
if (t <= 1) {
|
|
17
|
-
setOtpCode(authenticator.generate(selectedCipher.login.totp));
|
|
18
|
-
return 30;
|
|
19
|
-
}
|
|
20
|
-
return t - 1;
|
|
21
|
-
});
|
|
22
|
-
}, 1000);
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
setOtpCode("");
|
|
26
|
-
}
|
|
27
|
-
return () => {
|
|
28
|
-
if (interval)
|
|
29
|
-
clearInterval(interval);
|
|
30
|
-
};
|
|
31
|
-
}, [selectedCipher?.login?.totp]);
|
|
32
7
|
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 12, marginRight: 2, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Name:" }) }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.name, onChange: (value) => onChange({ ...selectedCipher, name: value }) }) })] }), selectedCipher.type === CipherType.Login && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 12, marginRight: 2, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Username:" }) }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.login?.username ?? "", onChange: (value) => onChange({
|
|
33
8
|
...selectedCipher,
|
|
34
9
|
login: { ...selectedCipher.login, username: value },
|
|
35
|
-
}) }) })] })), selectedCipher.type === CipherType.Login && (_jsxs(Box, { flexDirection: "row", children: [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
10
|
+
}) }) })] })), selectedCipher.type === CipherType.Login && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 12, marginRight: 2, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Password:" }) }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: _jsx(TextInput, { inline: true, isPassword: true, showPasswordOnFocus: true, isActive: isFocused, value: selectedCipher.login?.password ?? "", onChange: (value) => onChange({
|
|
11
|
+
...selectedCipher,
|
|
12
|
+
login: { ...selectedCipher.login, password: value },
|
|
13
|
+
}) }) })] })), selectedCipher.type === CipherType.Login && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 12, marginRight: 2, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "URL:" }) }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.login?.uris?.[0]?.uri ?? "", onChange: (value) => onChange({
|
|
39
14
|
...selectedCipher,
|
|
40
15
|
login: {
|
|
41
16
|
...selectedCipher.login,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { Cipher } from "../../clients/bw.js";
|
|
2
|
-
export declare function MainTab({ isFocused, selectedCipher, onChange, }: {
|
|
1
|
+
import { Cipher, CipherType } from "../../clients/bw.js";
|
|
2
|
+
export declare function MainTab({ isFocused, selectedCipher, mode, onChange, onTypeChange, }: {
|
|
3
3
|
isFocused: boolean;
|
|
4
4
|
selectedCipher: Cipher;
|
|
5
|
+
mode: "view" | "new";
|
|
5
6
|
onChange: (cipher: Cipher) => void;
|
|
7
|
+
onTypeChange?: (type: CipherType) => void;
|
|
6
8
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,31 +1,95 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { CipherType } from "../../clients/bw.js";
|
|
4
4
|
import { primaryLight } from "../../theme/style.js";
|
|
5
5
|
import { TextInput } from "../../components/TextInput.js";
|
|
6
|
+
import { TabButton } from "../../components/TabButton.js";
|
|
6
7
|
import { useEffect, useState } from "react";
|
|
7
|
-
import {
|
|
8
|
+
import { createGuardrails, generate, stringToBytes } from "otplib";
|
|
8
9
|
const OTP_INTERVAL = 30;
|
|
9
|
-
|
|
10
|
+
const OTP_GUARDRAILS = createGuardrails({
|
|
11
|
+
MIN_SECRET_BYTES: 1,
|
|
12
|
+
MAX_SECRET_BYTES: 1024,
|
|
13
|
+
});
|
|
14
|
+
function parseTotpConfig(value) {
|
|
15
|
+
if (!value.startsWith("otpauth://")) {
|
|
16
|
+
return { secret: value };
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(value);
|
|
20
|
+
const secret = url.searchParams.get("secret") ?? "";
|
|
21
|
+
const periodRaw = Number.parseInt(url.searchParams.get("period") ?? "", 10);
|
|
22
|
+
const digitsRaw = Number.parseInt(url.searchParams.get("digits") ?? "", 10);
|
|
23
|
+
const algorithmRaw = (url.searchParams.get("algorithm") ?? "").toLowerCase();
|
|
24
|
+
const period = Number.isFinite(periodRaw) && periodRaw > 0 ? periodRaw : undefined;
|
|
25
|
+
const digits = digitsRaw === 6 || digitsRaw === 7 || digitsRaw === 8
|
|
26
|
+
? digitsRaw
|
|
27
|
+
: undefined;
|
|
28
|
+
const algorithm = algorithmRaw === "sha1" ||
|
|
29
|
+
algorithmRaw === "sha256" ||
|
|
30
|
+
algorithmRaw === "sha512"
|
|
31
|
+
? algorithmRaw
|
|
32
|
+
: undefined;
|
|
33
|
+
return { secret, period, digits, algorithm };
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return { secret: value };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function normalizeBase32Secret(value) {
|
|
40
|
+
return value.replace(/\s+/g, "").toUpperCase();
|
|
41
|
+
}
|
|
42
|
+
function Field({ label, value, isFocused, onChange, isPassword, labelWidth = 12, maxLines = 3, }) {
|
|
43
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: labelWidth, flexShrink: 0, children: _jsxs(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: [label, ":"] }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, isPassword: isPassword, showPasswordOnFocus: isPassword, value: value, multiline: maxLines > 1, maxLines: maxLines, onChange: onChange }) })] }));
|
|
44
|
+
}
|
|
45
|
+
export function MainTab({ isFocused, selectedCipher, mode, onChange, onTypeChange, }) {
|
|
10
46
|
const [otpCode, setOtpCode] = useState("");
|
|
11
47
|
const [otpTimeout, setOtpTimeout] = useState(0);
|
|
12
|
-
const genOtp = () => {
|
|
13
|
-
if (
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
48
|
+
const genOtp = async (config) => {
|
|
49
|
+
if (config.secret) {
|
|
50
|
+
const normalizedSecret = normalizeBase32Secret(config.secret);
|
|
51
|
+
const options = {
|
|
52
|
+
guardrails: OTP_GUARDRAILS,
|
|
53
|
+
...(config.period ? { period: config.period } : {}),
|
|
54
|
+
...(config.digits ? { digits: config.digits } : {}),
|
|
55
|
+
...(config.algorithm ? { algorithm: config.algorithm } : {}),
|
|
56
|
+
};
|
|
57
|
+
try {
|
|
58
|
+
const totp = await generate({ ...options, secret: normalizedSecret });
|
|
59
|
+
if (selectedCipher.login) {
|
|
60
|
+
selectedCipher.login.currentTotp = totp;
|
|
61
|
+
}
|
|
62
|
+
setOtpCode(totp);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
try {
|
|
66
|
+
const totp = await generate({
|
|
67
|
+
...options,
|
|
68
|
+
secret: stringToBytes(config.secret),
|
|
69
|
+
});
|
|
70
|
+
if (selectedCipher.login) {
|
|
71
|
+
selectedCipher.login.currentTotp = totp;
|
|
72
|
+
}
|
|
73
|
+
setOtpCode(totp);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
setOtpCode("");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
17
79
|
}
|
|
18
80
|
};
|
|
19
81
|
useEffect(() => {
|
|
20
82
|
let interval = null;
|
|
21
83
|
if (selectedCipher?.login?.totp) {
|
|
22
|
-
|
|
23
|
-
|
|
84
|
+
const config = parseTotpConfig(selectedCipher.login.totp);
|
|
85
|
+
const intervalSeconds = config.period ?? OTP_INTERVAL;
|
|
86
|
+
void genOtp(config);
|
|
87
|
+
setOtpTimeout(intervalSeconds);
|
|
24
88
|
interval = setInterval(() => {
|
|
25
89
|
setOtpTimeout((t) => {
|
|
26
90
|
if (t <= 1) {
|
|
27
|
-
genOtp();
|
|
28
|
-
return
|
|
91
|
+
void genOtp(config);
|
|
92
|
+
return intervalSeconds;
|
|
29
93
|
}
|
|
30
94
|
return t - 1;
|
|
31
95
|
});
|
|
@@ -39,54 +103,29 @@ export function MainTab({ isFocused, selectedCipher, onChange, }) {
|
|
|
39
103
|
clearInterval(interval);
|
|
40
104
|
};
|
|
41
105
|
}, [selectedCipher?.login?.totp]);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
106
|
+
const updateIdentity = (patch) => onChange({ ...selectedCipher, identity: { ...selectedCipher.identity, ...patch } });
|
|
107
|
+
const updateCard = (patch) => onChange({ ...selectedCipher, card: { ...selectedCipher.card, ...patch } });
|
|
108
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [mode === "new" && (_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Box, { width: 10, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Type:" }) }), [
|
|
109
|
+
[CipherType.Login, "Login"],
|
|
110
|
+
[CipherType.SecureNote, "Note"],
|
|
111
|
+
[CipherType.Card, "Card"],
|
|
112
|
+
[CipherType.Identity, "Identity"],
|
|
113
|
+
].map(([t, label]) => (_jsx(TabButton, { borderLess: true, active: selectedCipher.type === t, onClick: () => onTypeChange?.(t), children: label }, t)))] })), _jsx(Field, { label: "Name", labelWidth: selectedCipher.type === CipherType.SSHKey ? 13 : 12, value: selectedCipher.name, isFocused: isFocused, onChange: (value) => onChange({ ...selectedCipher, name: value }) }), selectedCipher.type === CipherType.Login && (_jsx(Field, { label: "Username", value: selectedCipher.login?.username ?? "", isFocused: isFocused, onChange: (value) => onChange({
|
|
114
|
+
...selectedCipher,
|
|
115
|
+
login: { ...selectedCipher.login, username: value },
|
|
116
|
+
}) })), selectedCipher.type === CipherType.Login && (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [_jsx(Box, { width: 12, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Password:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isPassword: true, showPasswordOnFocus: true, isActive: isFocused, value: selectedCipher.login?.password ?? "", onChange: (value) => onChange({
|
|
46
117
|
...selectedCipher,
|
|
47
118
|
login: { ...selectedCipher.login, password: value },
|
|
48
|
-
}) }) })] }), selectedCipher.login?.totp && (_jsxs(Box, { flexDirection: "row",
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
selectedCipher.identity?.firstName && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 12, marginRight: 2, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "First Name:" }) }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.firstName ?? "", onChange: (value) => onChange({
|
|
59
|
-
...selectedCipher,
|
|
60
|
-
identity: { ...selectedCipher.identity, firstName: value },
|
|
61
|
-
}) }) })] })), selectedCipher.type === CipherType.Identity &&
|
|
62
|
-
selectedCipher.identity?.lastName && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 12, marginRight: 2, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Last Name:" }) }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.lastName ?? "", onChange: (value) => onChange({
|
|
63
|
-
...selectedCipher,
|
|
64
|
-
identity: { ...selectedCipher.identity, lastName: value },
|
|
65
|
-
}) }) })] })), selectedCipher.type === CipherType.Identity &&
|
|
66
|
-
selectedCipher.identity?.username && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 12, marginRight: 2, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Username:" }) }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.username ?? "", onChange: (value) => onChange({
|
|
67
|
-
...selectedCipher,
|
|
68
|
-
identity: { ...selectedCipher.identity, username: value },
|
|
69
|
-
}) }) })] })), selectedCipher.type === CipherType.Identity &&
|
|
70
|
-
selectedCipher.identity?.city && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 12, marginRight: 2, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "City:" }) }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.city ?? "", onChange: (value) => onChange({
|
|
71
|
-
...selectedCipher,
|
|
72
|
-
identity: { ...selectedCipher.identity, city: value },
|
|
73
|
-
}) }) })] })), selectedCipher.type === CipherType.Identity &&
|
|
74
|
-
selectedCipher.identity?.address1 && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 12, marginRight: 2, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Address:" }) }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.address1 ?? "", onChange: (value) => onChange({
|
|
75
|
-
...selectedCipher,
|
|
76
|
-
identity: { ...selectedCipher.identity, address1: value },
|
|
77
|
-
}) }) })] })), selectedCipher.type === CipherType.Identity &&
|
|
78
|
-
selectedCipher.identity?.country && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 12, marginRight: 2, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Country:" }) }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.country ?? "", onChange: (value) => onChange({
|
|
79
|
-
...selectedCipher,
|
|
80
|
-
identity: { ...selectedCipher.identity, country: value },
|
|
81
|
-
}) }) })] })), selectedCipher.type === CipherType.Identity &&
|
|
82
|
-
selectedCipher.identity?.email && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 12, marginRight: 2, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Email:" }) }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.email ?? "", onChange: (value) => onChange({
|
|
83
|
-
...selectedCipher,
|
|
84
|
-
identity: { ...selectedCipher.identity, email: value },
|
|
85
|
-
}) }) })] })), selectedCipher.type === CipherType.Identity &&
|
|
86
|
-
selectedCipher.identity?.phone && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 12, marginRight: 2, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Phone:" }) }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.phone ?? "", onChange: (value) => onChange({
|
|
87
|
-
...selectedCipher,
|
|
88
|
-
identity: { ...selectedCipher.identity, phone: value },
|
|
89
|
-
}) }) })] })), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 12, flexShrink: 0, marginRight: 2, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Notes:" }) }), _jsx(Box, { flexGrow: 1, minHeight: 7, children: _jsx(TextInput, { multiline: true, maxLines: 5, isActive: isFocused, value: selectedCipher.notes ?? "", onChange: (value) => onChange({
|
|
119
|
+
}) }) })] }), selectedCipher.login?.totp && (_jsxs(Box, { flexDirection: "row", width: 20, flexShrink: 0, children: [_jsx(Box, { flexShrink: 0, width: 12, children: _jsxs(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: ["OTP (", otpTimeout.toString().padStart(2, "0"), "s):"] }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: otpCode }) })] }))] })), selectedCipher.type === CipherType.Login && (_jsx(Field, { label: "URL", value: selectedCipher.login?.uris?.[0]?.uri ?? "", isFocused: isFocused, onChange: (value) => onChange({
|
|
120
|
+
...selectedCipher,
|
|
121
|
+
login: {
|
|
122
|
+
...selectedCipher.login,
|
|
123
|
+
uris: [
|
|
124
|
+
{ uri: value },
|
|
125
|
+
...selectedCipher.login.uris.slice(1),
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
}) })), selectedCipher.type === CipherType.Card && (_jsxs(_Fragment, { children: [_jsx(Field, { label: "Cardholder", value: selectedCipher.card?.cardholderName ?? "", isFocused: isFocused, onChange: (v) => updateCard({ cardholderName: v }) }), _jsx(Field, { label: "Number", value: selectedCipher.card?.number ?? "", isFocused: isFocused, isPassword: true, onChange: (v) => updateCard({ number: v }) }), _jsx(Field, { label: "Brand", value: selectedCipher.card?.brand ?? "", isFocused: isFocused, onChange: (v) => updateCard({ brand: v }) }), _jsxs(Box, { flexDirection: "row", gap: 2, children: [_jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [_jsx(Box, { width: 12, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Exp Month:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.card?.expMonth ?? "", onChange: (v) => updateCard({ expMonth: v }) }) })] }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [_jsx(Box, { width: 10, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Exp Year:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.card?.expYear ?? "", onChange: (v) => updateCard({ expYear: v }) }) })] })] }), _jsx(Field, { label: "CVV", value: selectedCipher.card?.code ?? "", isFocused: isFocused, isPassword: true, onChange: (v) => updateCard({ code: v }) })] })), selectedCipher.type === CipherType.Identity && (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "row", gap: 2, children: [_jsxs(Box, { flexDirection: "row", width: "50%", flexShrink: 0, children: [_jsx(Box, { width: 12, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Title:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.title ?? "", onChange: (v) => updateIdentity({ title: v }) }) })] }), _jsxs(Box, { flexDirection: "row", width: "50%", flexShrink: 0, children: [_jsx(Box, { width: 12, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "First Name:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.firstName ?? "", onChange: (v) => updateIdentity({ firstName: v }) }) })] })] }), _jsxs(Box, { flexDirection: "row", gap: 2, children: [_jsxs(Box, { flexDirection: "row", width: "50%", flexShrink: 0, children: [_jsx(Box, { width: 12, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Middle:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.middleName ?? "", onChange: (v) => updateIdentity({ middleName: v }) }) })] }), _jsxs(Box, { flexDirection: "row", width: "50%", flexShrink: 0, children: [_jsx(Box, { width: 12, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Last Name:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.lastName ?? "", onChange: (v) => updateIdentity({ lastName: v }) }) })] })] }), _jsxs(Box, { flexDirection: "row", gap: 2, children: [_jsxs(Box, { flexDirection: "row", width: "50%", flexShrink: 0, children: [_jsx(Box, { width: 12, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Username:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.username ?? "", onChange: (v) => updateIdentity({ username: v }) }) })] }), _jsxs(Box, { flexDirection: "row", width: "50%", flexShrink: 0, children: [_jsx(Box, { width: 12, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Company:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.company ?? "", onChange: (v) => updateIdentity({ company: v }) }) })] })] }), _jsxs(Box, { flexDirection: "row", gap: 2, children: [_jsxs(Box, { flexDirection: "row", width: "50%", flexShrink: 0, children: [_jsx(Box, { width: 12, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Email:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.email ?? "", onChange: (v) => updateIdentity({ email: v }) }) })] }), _jsxs(Box, { flexDirection: "row", width: "50%", flexShrink: 0, children: [_jsx(Box, { width: 12, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Phone:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.phone ?? "", onChange: (v) => updateIdentity({ phone: v }) }) })] })] })] })), selectedCipher.type === CipherType.SSHKey && (_jsxs(_Fragment, { children: [_jsx(Field, { label: "Private Key", labelWidth: 13, value: selectedCipher.sshKey?.privateKey ?? "", isFocused: isFocused }), _jsx(Field, { label: "Public Key", labelWidth: 13, value: selectedCipher.sshKey?.publicKey ?? "", isFocused: isFocused })] })), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: selectedCipher.type === CipherType.SSHKey ? 12 : 11, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Notes:" }) }), _jsx(Box, { flexGrow: 1, minHeight: 6, children: _jsx(TextInput, { multiline: true, maxLines: 5, isActive: isFocused, value: selectedCipher.notes ?? "", onChange: (value) => onChange({
|
|
90
129
|
...selectedCipher,
|
|
91
130
|
notes: value,
|
|
92
131
|
}) }) })] })] }));
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Cipher } from "../../clients/bw.js";
|
|
2
|
-
export
|
|
2
|
+
export type Organization = {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function MoreInfoTab({ isFocused, selectedCipher, organizations, onChange, }: {
|
|
3
7
|
isFocused: boolean;
|
|
4
8
|
selectedCipher: Cipher;
|
|
9
|
+
organizations: Organization[];
|
|
5
10
|
onChange: (cipher: Cipher) => void;
|
|
6
11
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,14 +1,71 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text, useStdout } from "ink";
|
|
2
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
3
|
+
import { useRef, useId, useState } from "react";
|
|
3
4
|
import { CipherType } from "../../clients/bw.js";
|
|
4
5
|
import { primaryLight } from "../../theme/style.js";
|
|
5
6
|
import { TextInput } from "../../components/TextInput.js";
|
|
6
|
-
|
|
7
|
+
import { useMouseTarget } from "../../hooks/use-mouse.js";
|
|
8
|
+
export function MoreInfoTab({ isFocused, selectedCipher, organizations, onChange, }) {
|
|
7
9
|
const { stdout } = useStdout();
|
|
8
|
-
|
|
10
|
+
const [orgCursor, setOrgCursor] = useState(0);
|
|
11
|
+
const canChangeOrg = !selectedCipher.organizationId;
|
|
12
|
+
const orgOptions = canChangeOrg ? [{ id: null, name: "None (Personal)" }, ...organizations.map((o) => ({ id: o.id, name: o.name }))] : [];
|
|
13
|
+
useInput((_input, key) => {
|
|
14
|
+
if (!canChangeOrg)
|
|
15
|
+
return;
|
|
16
|
+
if (key.upArrow)
|
|
17
|
+
setOrgCursor((c) => Math.max(0, c - 1));
|
|
18
|
+
else if (key.downArrow)
|
|
19
|
+
setOrgCursor((c) => Math.min(orgOptions.length - 1, c + 1));
|
|
20
|
+
else if (_input === " ") {
|
|
21
|
+
const selected = orgOptions[orgCursor];
|
|
22
|
+
onChange({ ...selectedCipher, organizationId: selected?.id, collectionIds: [] });
|
|
23
|
+
}
|
|
24
|
+
}, { isActive: isFocused && canChangeOrg });
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, height: stdout.rows - 18, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 9, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "ID:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.id ?? "" }) })] }), selectedCipher.type === CipherType.Login && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 9, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "TOTP:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, isPassword: true, showPasswordOnFocus: true, value: selectedCipher.login?.totp ?? "", onChange: (value) => onChange({
|
|
26
|
+
...selectedCipher,
|
|
27
|
+
login: { ...selectedCipher.login, totp: value },
|
|
28
|
+
}) }) })] })), canChangeOrg && organizations.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Organization:" }), orgOptions.map((opt, idx) => {
|
|
29
|
+
const checked = selectedCipher.organizationId === opt.id;
|
|
30
|
+
const isCursor = orgCursor === idx && isFocused;
|
|
31
|
+
return (_jsx(OrgCheckbox, { label: opt.name, isCursor: isCursor, checked: checked, onFocus: () => setOrgCursor(idx), onChange: () => onChange({ ...selectedCipher, organizationId: opt.id, collectionIds: [] }) }, opt.id ?? "__none"));
|
|
32
|
+
})] })), !!selectedCipher.organizationId && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 18, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Organization:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: "gray", children: organizations.find((o) => o.id === selectedCipher.organizationId)?.name ?? selectedCipher.organizationId }) })] })), !!selectedCipher.folderId && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 18, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Folder ID:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.folderId ?? "" }) })] })), selectedCipher.type === CipherType.Identity && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 9, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Address:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.address1 ?? "", onChange: (value) => onChange({
|
|
33
|
+
...selectedCipher,
|
|
34
|
+
identity: { ...selectedCipher.identity, address1: value },
|
|
35
|
+
}) }) })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 9, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "City:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.city ?? "", onChange: (value) => onChange({
|
|
36
|
+
...selectedCipher,
|
|
37
|
+
identity: { ...selectedCipher.identity, city: value },
|
|
38
|
+
}) }) })] }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, gap: 1, children: [_jsxs(Box, { flexDirection: "row", width: "40%", flexShrink: 0, children: [_jsx(Box, { width: 9, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "State:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.state ?? "", onChange: (value) => onChange({
|
|
39
|
+
...selectedCipher,
|
|
40
|
+
identity: { ...selectedCipher.identity, state: value },
|
|
41
|
+
}) }) })] }), _jsxs(Box, { flexDirection: "row", width: "60%", flexShrink: 0, children: [_jsx(Box, { width: 13, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Postal Code:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.postalCode ?? "", onChange: (value) => onChange({
|
|
42
|
+
...selectedCipher,
|
|
43
|
+
identity: {
|
|
44
|
+
...selectedCipher.identity,
|
|
45
|
+
postalCode: value,
|
|
46
|
+
},
|
|
47
|
+
}) }) })] })] }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, gap: 1, children: [_jsxs(Box, { flexDirection: "row", width: "40%", flexShrink: 0, children: [_jsx(Box, { width: 9, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Country:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.country ?? "", onChange: (value) => onChange({
|
|
48
|
+
...selectedCipher,
|
|
49
|
+
identity: { ...selectedCipher.identity, country: value },
|
|
50
|
+
}) }) })] }), _jsxs(Box, { flexDirection: "row", width: "60%", flexShrink: 0, children: [_jsx(Box, { width: 13, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "License:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.licenseNumber ?? "", onChange: (value) => onChange({
|
|
51
|
+
...selectedCipher,
|
|
52
|
+
identity: {
|
|
53
|
+
...selectedCipher.identity,
|
|
54
|
+
licenseNumber: value,
|
|
55
|
+
},
|
|
56
|
+
}) }) })] })] }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, gap: 1, children: [_jsxs(Box, { flexDirection: "row", width: "40%", flexShrink: 0, children: [_jsx(Box, { width: 9, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "SSN:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, isPassword: true, showPasswordOnFocus: true, value: selectedCipher.identity?.ssn ?? "", onChange: (value) => onChange({
|
|
57
|
+
...selectedCipher,
|
|
58
|
+
identity: { ...selectedCipher.identity, ssn: value },
|
|
59
|
+
}) }) })] }), _jsxs(Box, { flexDirection: "row", width: "60%", flexShrink: 0, children: [_jsx(Box, { width: 13, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Passport:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.identity?.passportNumber ?? "", onChange: (value) => onChange({
|
|
60
|
+
...selectedCipher,
|
|
61
|
+
identity: {
|
|
62
|
+
...selectedCipher.identity,
|
|
63
|
+
passportNumber: value,
|
|
64
|
+
},
|
|
65
|
+
}) }) })] })] })] })), selectedCipher.type === CipherType.SSHKey && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 13, flexShrink: 0, children: _jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Fingerprint:" }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: selectedCipher.sshKey?.keyFingerprint ?? "" }) })] })), !!selectedCipher.fields?.length && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Fields:" }), selectedCipher.fields?.map((field, idx) => (_jsxs(Box, { flexDirection: "row", paddingLeft: 2, children: [_jsx(Box, { width: 16, children: _jsxs(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: [field.name || idx, ":"] }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: field.value ?? "", onChange: (value) => {
|
|
9
66
|
const newFields = selectedCipher.fields?.map((f, i) => i === idx ? { ...f, value } : f);
|
|
10
67
|
onChange({ ...selectedCipher, fields: newFields });
|
|
11
|
-
} }) })] }, idx)))] })), !!selectedCipher.login?.uris?.length && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Uris:" }), selectedCipher.login.uris.map((uri, idx) => (_jsx(Box, { flexDirection: "row", paddingLeft: 2, children: _jsx(Box, { flexGrow: 1,
|
|
68
|
+
} }) })] }, idx)))] })), !!selectedCipher.login?.uris?.length && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: isFocused ? primaryLight : "gray", children: "Uris:" }), selectedCipher.login.uris.map((uri, idx) => (_jsx(Box, { flexDirection: "row", paddingLeft: 2, children: _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { inline: true, isActive: isFocused, value: uri.uri ?? "", onChange: (value) => {
|
|
12
69
|
const newUris = selectedCipher.login?.uris?.map((u, i) => i === idx ? { ...u, uri: value } : u);
|
|
13
70
|
onChange({
|
|
14
71
|
...selectedCipher,
|
|
@@ -16,3 +73,21 @@ export function MoreInfoTab({ isFocused, selectedCipher, onChange, }) {
|
|
|
16
73
|
});
|
|
17
74
|
} }) }) }, idx)))] }))] }));
|
|
18
75
|
}
|
|
76
|
+
function OrgCheckbox({ label, isCursor, checked, onChange, onFocus, }) {
|
|
77
|
+
const checkRef = useRef(null);
|
|
78
|
+
const labelRef = useRef(null);
|
|
79
|
+
const checkId = useId();
|
|
80
|
+
const labelId = useId();
|
|
81
|
+
useMouseTarget(checkId, checkRef, {
|
|
82
|
+
onClick: () => onChange(),
|
|
83
|
+
});
|
|
84
|
+
useMouseTarget(labelId, labelRef, {
|
|
85
|
+
onClick: () => onFocus(),
|
|
86
|
+
});
|
|
87
|
+
useInput((input) => {
|
|
88
|
+
if (input === " ") {
|
|
89
|
+
onChange();
|
|
90
|
+
}
|
|
91
|
+
}, { isActive: isCursor });
|
|
92
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { ref: checkRef, children: _jsx(Text, { color: isCursor ? "white" : "gray", bold: isCursor, children: checked ? "[x] " : "[ ] " }) }), _jsx(Box, { ref: labelRef, children: _jsx(Text, { color: isCursor ? "white" : "gray", children: label }) })] }));
|
|
93
|
+
}
|
package/dist/hooks/bw.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BwKeys, Client, SyncResponse } from "../clients/bw.js";
|
|
1
|
+
import { BwKeys, CipherType, Client, SyncResponse } from "../clients/bw.js";
|
|
2
2
|
interface BwConfig {
|
|
3
3
|
baseUrl?: string;
|
|
4
4
|
keys: BwKeys;
|
|
@@ -19,5 +19,6 @@ export declare const useBwSync: () => {
|
|
|
19
19
|
error: string | null;
|
|
20
20
|
fetchSync: (forceRefresh?: boolean) => Promise<void>;
|
|
21
21
|
};
|
|
22
|
+
export declare function createEmptyCipher(type?: CipherType): any;
|
|
22
23
|
export declare const emptyCipher: any;
|
|
23
24
|
export {};
|
package/dist/hooks/bw.js
CHANGED
|
@@ -123,15 +123,60 @@ export const useBwSync = () => {
|
|
|
123
123
|
}, [fetchSync]);
|
|
124
124
|
return { sync, error, fetchSync };
|
|
125
125
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
username: null,
|
|
132
|
-
password: null,
|
|
133
|
-
uris: [],
|
|
134
|
-
},
|
|
135
|
-
fields: [],
|
|
136
|
-
organizationId: null,
|
|
126
|
+
const emptyLogin = {
|
|
127
|
+
username: null,
|
|
128
|
+
password: null,
|
|
129
|
+
totp: null,
|
|
130
|
+
uris: [],
|
|
137
131
|
};
|
|
132
|
+
const emptyCard = {
|
|
133
|
+
cardholderName: null,
|
|
134
|
+
brand: null,
|
|
135
|
+
number: null,
|
|
136
|
+
expMonth: null,
|
|
137
|
+
expYear: null,
|
|
138
|
+
code: null,
|
|
139
|
+
};
|
|
140
|
+
const emptyIdentity = {
|
|
141
|
+
title: null,
|
|
142
|
+
firstName: null,
|
|
143
|
+
middleName: null,
|
|
144
|
+
lastName: null,
|
|
145
|
+
username: null,
|
|
146
|
+
company: null,
|
|
147
|
+
email: null,
|
|
148
|
+
phone: null,
|
|
149
|
+
address1: null,
|
|
150
|
+
address2: null,
|
|
151
|
+
address3: null,
|
|
152
|
+
city: null,
|
|
153
|
+
state: null,
|
|
154
|
+
postalCode: null,
|
|
155
|
+
country: null,
|
|
156
|
+
ssn: null,
|
|
157
|
+
passportNumber: null,
|
|
158
|
+
licenseNumber: null,
|
|
159
|
+
};
|
|
160
|
+
export function createEmptyCipher(type = CipherType.Login) {
|
|
161
|
+
const base = {
|
|
162
|
+
name: "",
|
|
163
|
+
type,
|
|
164
|
+
notes: null,
|
|
165
|
+
fields: [],
|
|
166
|
+
organizationId: null,
|
|
167
|
+
collectionIds: [],
|
|
168
|
+
};
|
|
169
|
+
switch (type) {
|
|
170
|
+
case CipherType.Login:
|
|
171
|
+
return { ...base, login: { ...emptyLogin } };
|
|
172
|
+
case CipherType.SecureNote:
|
|
173
|
+
return { ...base, secureNote: { type: 0 } };
|
|
174
|
+
case CipherType.Card:
|
|
175
|
+
return { ...base, card: { ...emptyCard } };
|
|
176
|
+
case CipherType.Identity:
|
|
177
|
+
return { ...base, identity: { ...emptyIdentity } };
|
|
178
|
+
default:
|
|
179
|
+
return base;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
export const emptyCipher = createEmptyCipher();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bitty-tui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": "https://github.com/mceck/bitty",
|
|
6
6
|
"keywords": [
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"argon2": "^0.44.0",
|
|
33
33
|
"chalk": "^5.6.2",
|
|
34
|
-
"clipboardy": "^
|
|
34
|
+
"clipboardy": "^5.3.1",
|
|
35
35
|
"ink": "^6.3.0",
|
|
36
|
-
"otplib": "^
|
|
36
|
+
"otplib": "^13.3.0",
|
|
37
37
|
"react": "^19.1.0",
|
|
38
38
|
"read-package-up": "^12.0.0"
|
|
39
39
|
},
|
package/readme.md
CHANGED
|
@@ -25,10 +25,7 @@ If you check "Remember me" during login, your vault encryption keys will be stor
|
|
|
25
25
|
|
|
26
26
|
## TODO
|
|
27
27
|
|
|
28
|
-
- Collections support
|
|
29
28
|
- Test Fido, Duo MFA support
|
|
30
|
-
- Handle more fields editing
|
|
31
|
-
- Handle creating different cipher types
|
|
32
29
|
|
|
33
30
|
## Acknowledgments
|
|
34
31
|
- [Bitwarden whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper)
|