diff --git a/.eslintignore b/.eslintignore index 3c3629e64..6e4c02cef 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,3 @@ -node_modules +**/node_modules +**/android +**/ios \ No newline at end of file diff --git a/apps/shinkai-visor/src/components/add-agent/add-agent.tsx b/apps/shinkai-visor/src/components/add-agent/add-agent.tsx index 636995ee4..1ee086b5c 100644 --- a/apps/shinkai-visor/src/components/add-agent/add-agent.tsx +++ b/apps/shinkai-visor/src/components/add-agent/add-agent.tsx @@ -8,6 +8,7 @@ import { z } from 'zod'; import { useAuth } from '../../store/auth/auth'; import { useUIContainer } from '../../store/ui-container/ui-container'; +import { Header } from '../header/header'; import { Button } from '../ui/button'; import { Form, @@ -101,12 +102,10 @@ export const AddAgent = () => { }; return (
-
- -

- -

-
+
} + title={} + />
; - -type AddNodeDataFromQr = Pick< - FormType, - | 'registrationCode' - | 'nodeAddress' - | 'shinkaiIdentity' - | 'nodeEncryptionPublicKey' - | 'nodeSignaturePublicKey' ->; - -enum AddNodeSteps { - ScanQR = 0, - Connect, -} - -export const AddNode = () => { - const history = useHistory(); - const setAuth = useAuth((state) => state.setAuth); - const DEFAULT_NODE_ADDRESS = 'http://127.0.0.1:9550'; - // TODO: This value should be obtained from node - const DEFAULT_SHINKAI_IDENTITY = '@@node1.shinkai'; - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - registrationCode: '', - registrationName: 'main_device', - permissionType: 'admin', - identityType: 'device', - profile: 'main', - nodeAddress: '', - shinkaiIdentity: '', - nodeEncryptionPublicKey: '', - nodeSignaturePublicKey: '', - profileEncryptionPublicKey: '', - profileSignaturePublicKey: '', - myDeviceEncryptionPublicKey: '', - myDeviceIdentityPublicKey: '', - profileEncryptionSharedKey: '', - profileSignatureSharedKey: '', - myDeviceEncryptionSharedKey: '', - myDeviceIdentitySharedKey: undefined, - }, - }); - const { - isLoading, - mutateAsync: submitRegistration, - isError: isSubmitError, - error: submitError, - } = useSubmitRegistrationNoCode({ - onSuccess: (response) => { - if (response.success) { - const values = form.getValues(); - setAuth({ - profile: values.profile, - permission_type: values.permissionType, - node_address: values.nodeAddress, - shinkai_identity: values.shinkaiIdentity, - node_signature_pk: - response.data?.identity_public_key ?? - values.nodeSignaturePublicKey ?? - '', - node_encryption_pk: - response.data?.encryption_public_key ?? - values.nodeEncryptionPublicKey ?? - '', - registration_name: values.registrationName, - my_device_identity_pk: values.myDeviceIdentityPublicKey, - my_device_identity_sk: values.myDeviceIdentitySharedKey, - my_device_encryption_pk: values.myDeviceEncryptionPublicKey, - my_device_encryption_sk: values.myDeviceEncryptionSharedKey, - profile_identity_pk: values.profileSignaturePublicKey, - profile_identity_sk: values.profileSignatureSharedKey, - profile_encryption_pk: values.profileEncryptionPublicKey, - profile_encryption_sk: values.profileEncryptionSharedKey, - }); - history.replace('/inboxes'); - } else { - throw new Error('Failed to submit registration'); - } - }, - }); - - const fileInput = useRef(null); - const [currentStep, setCurrentStep] = useState( - AddNodeSteps.ScanQR - ); - - const onFileInputClick = () => { - fileInput.current?.click(); - }; - - const onQrImageSelected: React.ChangeEventHandler = async ( - event - ): Promise => { - if (!event.target.files || !event.target.files[0]) { - return; - } - const qrImageUrl = URL.createObjectURL(event.target.files[0]); - const codeReader = new BrowserQRCodeReader(); - const resultImage = await codeReader.decodeFromImageUrl(qrImageUrl); - const json_string = resultImage.getText(); - const parsedQrData: QRSetupData = JSON.parse(json_string); - const nodeDataFromQr = getValuesFromQr(parsedQrData); - form.reset((prev) => ({ ...prev, ...nodeDataFromQr })); - setCurrentStep(AddNodeSteps.Connect); - }; - - const generateDeviceEncryptionKeys = async (): Promise< - Pick< - FormType, - 'myDeviceEncryptionPublicKey' | 'myDeviceEncryptionSharedKey' - > - > => { - const seed = crypto.getRandomValues(new Uint8Array(32)); - const { my_encryption_pk_string, my_encryption_sk_string } = - await generateEncryptionKeys(seed); - return { - myDeviceEncryptionPublicKey: my_encryption_pk_string, - myDeviceEncryptionSharedKey: my_encryption_sk_string, - }; - }; - - const generateDeviceSignatureKeys = async (): Promise< - Pick - > => { - const { my_identity_pk_string, my_identity_sk_string } = - await generateSignatureKeys(); - return { - myDeviceIdentityPublicKey: my_identity_pk_string, - myDeviceIdentitySharedKey: my_identity_sk_string, - }; - }; - - const generateProfileEncryptionKeys = async (): Promise< - Pick - > => { - const seed = crypto.getRandomValues(new Uint8Array(32)); - const { my_encryption_pk_string, my_encryption_sk_string } = - await generateEncryptionKeys(seed); - return { - profileEncryptionPublicKey: my_encryption_pk_string, - profileEncryptionSharedKey: my_encryption_sk_string, - }; - }; - - const generateProfileSignatureKeys = async (): Promise< - Pick - > => { - const { my_identity_pk_string, my_identity_sk_string } = - await generateSignatureKeys(); - return { - profileSignaturePublicKey: my_identity_pk_string, - profileSignatureSharedKey: my_identity_sk_string, - }; - }; - - const getValuesFromQr = (qrData: QRSetupData): AddNodeDataFromQr => { - return { - registrationCode: qrData.registration_code, - nodeAddress: qrData.node_address, - shinkaiIdentity: qrData.shinkai_identity, - nodeEncryptionPublicKey: qrData.node_encryption_pk, - nodeSignaturePublicKey: qrData.node_signature_pk, - }; - }; - - const connect = (values: FormType) => { - submitRegistration({ - registration_code: values.registrationCode ?? '', - profile: values.profile, - identity_type: values.identityType, - permission_type: values.permissionType, - node_address: values.nodeAddress, - shinkai_identity: values.shinkaiIdentity, - node_encryption_pk: values.nodeEncryptionPublicKey ?? '', - registration_name: values.registrationName, - my_device_identity_sk: values.myDeviceIdentitySharedKey, - my_device_encryption_sk: values.myDeviceEncryptionSharedKey, - profile_identity_sk: values.profileSignatureSharedKey, - profile_encryption_sk: values.profileEncryptionSharedKey, - }); - }; - - useEffect(() => { - Promise.all([ - generateDeviceEncryptionKeys(), - generateDeviceSignatureKeys(), - generateProfileEncryptionKeys(), - generateProfileSignatureKeys(), - ]).then( - ([ - deviceEncryption, - deviceSignature, - profileEncryption, - profileSignature, - ]) => { - form.reset((prevInitialValues) => ({ - ...prevInitialValues, - ...deviceEncryption, - ...deviceSignature, - ...profileEncryption, - ...profileSignature, - })); - } - ); - }, [form]); - - useEffect(() => { - fetch(`${DEFAULT_NODE_ADDRESS}/v1/shinkai_health`) - .then((response) => response.json()) - .then((data) => { - if (data.status === 'ok') { - form.setValue('nodeAddress', DEFAULT_NODE_ADDRESS); - form.setValue('shinkaiIdentity', DEFAULT_SHINKAI_IDENTITY); - setCurrentStep(AddNodeSteps.Connect); - } - }) - .catch((error) => console.error('error polling', error)); - }, [form]); - - return ( -
- Connect -
- {currentStep === AddNodeSteps.ScanQR && ( -
-
- -
- -
- - onQrImageSelected(event)} - ref={fileInput} - type="file" - /> -
-
- )} - - {currentStep === AddNodeSteps.Connect && ( - - -
- ( - - - - - - - - - - )} - /> - - ( - - - - - - - - - - )} - /> - {isSubmitError && ( - - )} -
- - - - - )} -
-
- ); -}; diff --git a/apps/shinkai-visor/src/components/agents/agents.tsx b/apps/shinkai-visor/src/components/agents/agents.tsx index 7e110c5f2..eb2d08638 100644 --- a/apps/shinkai-visor/src/components/agents/agents.tsx +++ b/apps/shinkai-visor/src/components/agents/agents.tsx @@ -5,6 +5,7 @@ import { FormattedMessage } from 'react-intl'; import { useAuth } from '../../store/auth/auth'; import { EmptyAgents } from '../empty-agents/empty-agents'; +import { Header } from '../header/header'; import { Button } from '../ui/button'; import { ScrollArea } from '../ui/scroll-area'; @@ -20,27 +21,27 @@ export const Agents = () => { profile_encryption_sk: auth?.profile_encryption_sk ?? '', profile_identity_sk: auth?.profile_identity_sk ?? '', }); - return !agents?.length ? ( -
- -
- ) : ( + return (
-
- -

- -

-
- - {agents?.map((agent) => ( - - - - ))} - +
} + title={} + /> + {!agents?.length ? ( +
+ +
+ ) : ( + + {agents?.map((agent) => ( + + + + ))} + + )}
); }; diff --git a/apps/shinkai-visor/src/components/connect-method-qr-code/connec-method-qr-code.tsx b/apps/shinkai-visor/src/components/connect-method-qr-code/connec-method-qr-code.tsx new file mode 100644 index 000000000..39edd90fa --- /dev/null +++ b/apps/shinkai-visor/src/components/connect-method-qr-code/connec-method-qr-code.tsx @@ -0,0 +1,305 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { QRSetupData } from '@shinkai_network/shinkai-message-ts/models'; +import { useSubmitRegistrationNoCode } from '@shinkai_network/shinkai-node-state/lib/mutations/submitRegistation/useSubmitRegistrationNoCode'; +import { BrowserQRCodeReader } from '@zxing/browser'; +import { Loader2, PlugZap, QrCode, Trash, Upload } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { FormattedMessage } from 'react-intl'; +import { useHistory } from 'react-router-dom'; +import * as z from 'zod'; + +import { generateMyEncryptionKeys } from '../../helpers/encryption-keys'; +import { SetupData, useAuth } from '../../store/auth/auth'; +import { Header } from '../header/header'; +import { Button } from '../ui/button'; +import ErrorMessage from '../ui/error-message'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '../ui/form'; +import { Input } from '../ui/input'; + +const formSchema = z.object({ + registration_code: z.string().min(5), + registration_name: z.string().min(5), + permission_type: z.enum(['admin']), + identity_type: z.enum(['device']), + profile: z.enum(['main']), + node_address: z.string().url(), + shinkai_identity: z.string().min(11), + node_encryption_pk: z.string().optional(), + node_signature_pk: z.string().optional(), + profile_encryption_pk: z.string().min(5), + profile_identity_pk: z.string().min(5), + my_device_encryption_pk: z.string().min(5), + my_device_identity_pk: z.string().min(5), + profile_encryption_sk: z.string().min(5), + profile_identity_sk: z.string().min(5), + my_device_encryption_sk: z.string().min(5), + my_device_identity_sk: z.string().min(5), +}); + +type FormType = z.infer; + +type AddNodeDataFromQr = Pick< + FormType, + | 'registration_code' + | 'node_address' + | 'shinkai_identity' + | 'node_encryption_pk' + | 'node_signature_pk' +>; + +export const ConnectMethodQrCode = () => { + const history = useHistory(); + const setAuth = useAuth((state) => state.setAuth); + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + registration_code: '', + registration_name: 'main_device', + permission_type: 'admin', + identity_type: 'device', + profile: 'main', + node_address: '', + shinkai_identity: '', + node_encryption_pk: '', + node_signature_pk: '', + profile_encryption_pk: '', + profile_encryption_sk: '', + my_device_encryption_pk: '', + my_device_encryption_sk: '', + profile_identity_pk: '', + profile_identity_sk: '', + my_device_identity_pk: '', + my_device_identity_sk: '', + }, + }); + const [qrImageFile, setQRImageFile] = useState(null); + const [qrImageUrl, setQRImageUrl] = useState(null); + + const { + isLoading, + mutateAsync: submitRegistration, + isError: isSubmitError, + error: submitError, + } = useSubmitRegistrationNoCode({ + onSuccess: (response) => { + if (response.success) { + const values = form.getValues(); + const authData: SetupData = { + ...values, + node_signature_pk: values.node_signature_pk ?? '', + node_encryption_pk: values.node_encryption_pk ?? '', + }; + authSuccess(authData); + } else { + throw new Error('Failed to submit registration'); + } + }, + }); + + const onQRImageSelected: React.ChangeEventHandler = async ( + event + ): Promise => { + if (!event.target.files || !event.target.files[0]) { + return; + } + const file = event.target.files[0]; + const qrImageUrl = URL.createObjectURL(file); + const codeReader = new BrowserQRCodeReader(); + const resultImage = await codeReader.decodeFromImageUrl(qrImageUrl); + const jsonString = resultImage.getText(); + const parsedQrData: QRSetupData = JSON.parse(jsonString); + const nodeDataFromQr = getValuesFromQr(parsedQrData); + setQRImageFile(file); + form.reset((prev) => ({ ...prev, ...nodeDataFromQr })); + }; + + const getValuesFromQr = (qrData: QRSetupData): AddNodeDataFromQr => { + return { + ...qrData, + }; + }; + + const connect = (values: FormType) => { + submitRegistration({ + ...values, + registration_code: values.registration_code ?? '', + node_encryption_pk: values.node_encryption_pk ?? '', + }); + }; + + const authSuccess = (setupData: SetupData) => { + setAuth(setupData); + history.replace('/inboxes'); + }; + + useEffect(() => { + console.log('generate keys'); + generateMyEncryptionKeys().then((encryptionKeys) => { + form.reset((prevInitialValues) => ({ + ...prevInitialValues, + ...encryptionKeys, + })); + }); + }, [form]); + + useEffect(() => { + if (qrImageFile) { + const qrImageUrl = URL.createObjectURL(qrImageFile); + setQRImageUrl(qrImageUrl); + } else { + setQRImageUrl(''); + } + }, [qrImageFile]); + + const removeQRFile = () => { + setQRImageFile(null); + form.reset(); + }; + + return ( +
+
+ } + icon={} + title={ + + } + /> + +
+ +
+
+ QR code +
+
+ {qrImageFile && qrImageUrl ? ( +
+
+ + + {qrImageFile.name} + +
+
+ qr connection data + +
+
+ ) : ( + + )} +
+
+
+ {qrImageFile && ( + <> + ( + + + + + + + + + + )} + /> + + ( + + + + + + + + + + )} + /> + + ( + + + + + + + + + + )} + /> + + )} + + {isSubmitError && } +
+ +
+ +
+ ); +}; diff --git a/apps/shinkai-visor/src/components/connect-method-quick-start/connect-method-quick-start.tsx b/apps/shinkai-visor/src/components/connect-method-quick-start/connect-method-quick-start.tsx new file mode 100644 index 000000000..e70d4a251 --- /dev/null +++ b/apps/shinkai-visor/src/components/connect-method-quick-start/connect-method-quick-start.tsx @@ -0,0 +1,209 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useSubmitRegistrationNoCode } from '@shinkai_network/shinkai-node-state/lib/mutations/submitRegistation/useSubmitRegistrationNoCode'; +import { FileKey, Loader2, PlugZap, QrCode, Zap } from 'lucide-react'; +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { FormattedMessage } from 'react-intl'; +import { useHistory } from 'react-router'; +import { z } from 'zod'; + +import { + Encryptionkeys, + generateMyEncryptionKeys, +} from '../../helpers/encryption-keys'; +import { SetupData, useAuth } from '../../store/auth/auth'; +import { ConnectionMethodOption } from '../connection-method-option/connection-method-option'; +import { Header } from '../header/header'; +import { Button } from '../ui/button'; +import ErrorMessage from '../ui/error-message'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '../ui/form'; +import { Input } from '../ui/input'; + +const formSchema = z.object({ + registration_name: z.string().min(5), + node_address: z.string().url(), + shinkai_identity: z.string().min(11), +}); + +type FormType = z.infer; + +export const ConnectMethodQuickStart = () => { + const history = useHistory(); + const setAuth = useAuth((state) => state.setAuth); + const DEFAULT_NODE_ADDRESS = 'http://127.0.0.1:9550'; + const DEFAULT_SHINKAI_IDENTITY = '@@localhost.shinkai'; + const [encryptionKeys, setEncryptedKeys] = useState( + null + ); + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + registration_name: 'main_device', + node_address: DEFAULT_NODE_ADDRESS, + shinkai_identity: DEFAULT_SHINKAI_IDENTITY, + }, + }); + const { + isLoading, + mutateAsync: submitRegistration, + isError: isSubmitError, + error: submitError, + } = useSubmitRegistrationNoCode({ + onSuccess: (response, setupPayload) => { + console.log(response); + if (response.success && encryptionKeys) { + const authData: SetupData = { + ...encryptionKeys, + ...setupPayload, + node_signature_pk: response.data?.identity_public_key ?? '', + node_encryption_pk: response.data?.encryption_public_key ?? '', + }; + setAuth(authData); + history.replace('/inboxes'); + } else { + throw new Error('Failed to submit registration'); + } + }, + }); + + const connect = async (values: FormType) => { + let keys = encryptionKeys; + if (!keys) { + keys = await generateMyEncryptionKeys(); + setEncryptedKeys(keys); + } + submitRegistration({ + profile: 'main', + identity_type: 'device', + permission_type: 'admin', + shinkai_identity: values.shinkai_identity, + registration_code: '', + node_encryption_pk: '', + node_address: values.node_address, + registration_name: values.registration_name, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ...keys, + }); + }; + + const selectQRCodeMethod = () => { + history.push('/nodes/connect/method/qr-code'); + }; + const selectRestoreMethod = () => { + history.push('/nodes/connect/method/restore-connection'); + }; + + return ( +
+
+ } + icon={} + title={ + + } + /> +
+ + ( + + + + + + + + + + )} + /> + + ( + + + + + + + + + + )} + /> + + ( + + + + + + + + + + )} + /> + + {isSubmitError && } + + + + + +
+ + + + + + } + icon={} + onClick={() => selectQRCodeMethod()} + title={ + + } + /> + + + } + icon={} + onClick={() => selectRestoreMethod()} + title={ + + } + /> +
+
+ ); +}; diff --git a/apps/shinkai-visor/src/components/connect-method-restore-connection/connect-method-restore-connection.tsx b/apps/shinkai-visor/src/components/connect-method-restore-connection/connect-method-restore-connection.tsx new file mode 100644 index 000000000..4831153c9 --- /dev/null +++ b/apps/shinkai-visor/src/components/connect-method-restore-connection/connect-method-restore-connection.tsx @@ -0,0 +1,198 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { decryptMessageWithPassphrase } from '@shinkai_network/shinkai-message-ts/cryptography'; +import { FileKey, PlugZap, Trash, Upload } from 'lucide-react'; +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { FormattedMessage } from 'react-intl'; +import { useHistory } from 'react-router'; +import { z } from 'zod'; + +import { useAuth } from '../../store/auth/auth'; +import { Header } from '../header/header'; +import { Button } from '../ui/button'; +import ErrorMessage from '../ui/error-message'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '../ui/form'; +import { Input } from '../ui/input'; + +const formSchema = z.object({ + encryptedConnection: z.string().min(1), + passphrase: z.string().min(8), +}); + +type FormType = z.infer; + +export const ConnectMethodRestoreConnection = () => { + const history = useHistory(); + const setAuth = useAuth((state) => state.setAuth); + const [error, setError] = useState(false); + const [encryptedConnectionFile, setEncryptedConnectionFile] = + useState(null); + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + encryptedConnection: '', + passphrase: '', + }, + }); + const onConnectionFileSelected: React.ChangeEventHandler< + HTMLInputElement + > = async (event): Promise => { + if (!event.target.files || !event.target.files[0]) { + return; + } + const file = event.target.files[0]; + const reader = new FileReader(); + reader.readAsText(file, 'UTF-8'); + reader.onload = (event) => { + console.log('onload event', event); + if (event?.target?.readyState !== event?.target?.DONE) { + return; + } + const encryptedConnection = event?.target?.result as string; + if (!encryptedConnection.startsWith('encrypted:')) { + return; + } + setEncryptedConnectionFile(file); + form.setValue('encryptedConnection', encryptedConnection); + }; + }; + const restore = async (values: FormType) => { + try { + const decryptedValue = await decryptMessageWithPassphrase( + values.encryptedConnection, + values.passphrase + ); + if (decryptedValue) { + const decryptedSetupData = JSON.parse(decryptedValue); + + setAuth(decryptedSetupData); + // TODO: Add logic to test if setup data is valid to create an authenticated connection with Shinkai Node + history.replace('/inboxes'); + } + } catch (_) { + setError(true); + } + }; + const removeConnectionFile = () => { + form.setValue('encryptedConnection', ''); + setEncryptedConnectionFile(null); + }; + return ( +
+
+ } + icon={} + title={ + + } + /> + +
+ +
+ ( + + + + + +
+
+ {encryptedConnectionFile ? ( +
+
+ + + {encryptedConnectionFile.name} + +
+ +
+ ) : ( + + )} +
+ {encryptedConnectionFile && ( + + )} +
+
+ +
+ )} + /> + + ( + + + + + + + + + + )} + /> + {error && } +
+ + +
+ +
+ ); +}; diff --git a/apps/shinkai-visor/src/components/connection-method-option/connection-method-option.tsx b/apps/shinkai-visor/src/components/connection-method-option/connection-method-option.tsx new file mode 100644 index 000000000..0ec445ab6 --- /dev/null +++ b/apps/shinkai-visor/src/components/connection-method-option/connection-method-option.tsx @@ -0,0 +1,33 @@ +import { ReactNode } from 'react'; + +export type ConnectionMethodOptionProps = { + icon: ReactNode; + title: ReactNode; + description: ReactNode; + onClick?: () => void; +}; + +export const ConnectionMethodOption = ({ + icon, + title, + description, + onClick, +}: ConnectionMethodOptionProps) => { + const onConnectionMethodOptionClick = () => { + if (typeof onClick === 'function') { + onClick(); + } + }; + return ( +
onConnectionMethodOptionClick()} + > +
{icon}
+
+

{title}

+

{description}

+
+
+ ); +}; diff --git a/apps/shinkai-visor/src/components/create-inbox/create-inbox.tsx b/apps/shinkai-visor/src/components/create-inbox/create-inbox.tsx index 07825238f..7aaef723f 100644 --- a/apps/shinkai-visor/src/components/create-inbox/create-inbox.tsx +++ b/apps/shinkai-visor/src/components/create-inbox/create-inbox.tsx @@ -8,6 +8,7 @@ import { useHistory } from 'react-router-dom'; import { z } from 'zod'; import { useAuth } from '../../store/auth/auth'; +import { Header } from '../header/header'; import { Button } from '../ui/button'; import { Form, @@ -68,12 +69,10 @@ export const CreateInbox = () => { return (
-
- -

- -

-
+
} + title={} + >
{ return (
-
- -

- -

-
+
} + title={} + /> { + const intl = useIntl(); + const formSchema = z + .object({ + passphrase: z.string().min(8), + confirmPassphrase: z.string().min(8), + }) + .superRefine(({ passphrase, confirmPassphrase }, ctx) => { + if (passphrase !== confirmPassphrase) { + ctx.addIssue({ + code: 'custom', + message: intl.formatMessage({ id: 'passphrases-dont-match' }), + path: ['confirmPassphrase'], + }); + } + }); + type FormSchemaType = z.infer; + const auth = useAuth((state) => state.auth); + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + passphrase: '', + confirmPassphrase: '', + }, + }); + const passphrase = form.watch('passphrase'); + const confirmPassphrase = form.watch('confirmPassphrase'); + const [encryptedSetupData, setEncryptedSetupData] = useState(''); + useEffect(() => { + setEncryptedSetupData(''); + }, [passphrase, confirmPassphrase, setEncryptedSetupData]); + const exportConnection = async (values: FormSchemaType): Promise => { + // TODO: Convert to a common format shared by visor, app and tray + const parsedSetupData = JSON.stringify(auth); + const encryptedSetupData = await encryptMessageWithPassphrase( + parsedSetupData, + values.passphrase + ); + setEncryptedSetupData(encryptedSetupData); + }; + const download = (): void => { + const link = document.createElement('a'); + const content = encryptedSetupData; + const file = new Blob([content], { type: 'text/plain' }); + link.href = URL.createObjectURL(file); + link.download = `${auth?.registration_name}.shinkai.key`; + link.click(); + URL.revokeObjectURL(link.href); + }; + return ( +
+
} + title={} + /> +
+ + +
+ ( + + + + + + + + + + )} + /> + ( + + + + + + + + + + )} + /> +
+ + + + + {encryptedSetupData && ( +
+
+ + + + + + +
+
+
download()}> + +
+ +
+
+ )} +
+
+ ); +}; diff --git a/apps/shinkai-visor/src/components/header/header.tsx b/apps/shinkai-visor/src/components/header/header.tsx new file mode 100644 index 000000000..1097ad87c --- /dev/null +++ b/apps/shinkai-visor/src/components/header/header.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from 'react'; + +export type HeaderProps = { + icon: ReactNode; + title: ReactNode | string; + description?: ReactNode | string; +}; + +export const Header = ({ icon, title, description }: HeaderProps) => { + return ( +
+
+ {icon} +
{title}
+
+ {description && {description}} +
+ ); +}; diff --git a/apps/shinkai-visor/src/components/inbox/inbox.tsx b/apps/shinkai-visor/src/components/inbox/inbox.tsx index d5be155e8..654b899fe 100644 --- a/apps/shinkai-visor/src/components/inbox/inbox.tsx +++ b/apps/shinkai-visor/src/components/inbox/inbox.tsx @@ -204,8 +204,8 @@ export const Inbox = () => { return (
-
- +
+

diff --git a/apps/shinkai-visor/src/components/inboxes/inboxes.css b/apps/shinkai-visor/src/components/inboxes/inboxes.css deleted file mode 100644 index 75b885059..000000000 --- a/apps/shinkai-visor/src/components/inboxes/inboxes.css +++ /dev/null @@ -1,7 +0,0 @@ -#shinkai-popup-root { - .inbox-id-container { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } -} diff --git a/apps/shinkai-visor/src/components/inboxes/inboxes.tsx b/apps/shinkai-visor/src/components/inboxes/inboxes.tsx index 5674e87b5..85d7e46bc 100644 --- a/apps/shinkai-visor/src/components/inboxes/inboxes.tsx +++ b/apps/shinkai-visor/src/components/inboxes/inboxes.tsx @@ -1,5 +1,3 @@ -import './inboxes.css'; - import { isJobInbox } from '@shinkai_network/shinkai-message-ts/utils'; import { useAgents } from '@shinkai_network/shinkai-node-state/lib/queries/getAgents/useGetAgents'; import { useGetInboxes } from '@shinkai_network/shinkai-node-state/lib/queries/getInboxes/useGetInboxes'; @@ -11,6 +9,7 @@ import { useHistory } from 'react-router-dom'; import { useAuth } from '../../store/auth/auth'; import { EmptyAgents } from '../empty-agents/empty-agents'; import { EmptyInboxes } from '../empty-inboxes/empty-inboxes'; +import { Header } from '../header/header'; import { Button } from '../ui/button'; import { ScrollArea } from '../ui/scroll-area'; @@ -46,13 +45,12 @@ export const Inboxes = () => { return (
-
- -

+
} + title={ -

-
- + } + /> {!agents?.length ? ( ) : !inboxIds?.length ? ( diff --git a/apps/shinkai-visor/src/components/nav/nav.tsx b/apps/shinkai-visor/src/components/nav/nav.tsx index dfea03a36..a7f65b856 100644 --- a/apps/shinkai-visor/src/components/nav/nav.tsx +++ b/apps/shinkai-visor/src/components/nav/nav.tsx @@ -1,4 +1,14 @@ -import { ArrowLeft, Bot, Inbox, LogOut, Menu, MessageCircle, Workflow, X } from 'lucide-react'; +import { + ArrowLeft, + Bot, + Inbox, + LogOut, + Menu, + MessageCircle, + Settings, + Workflow, + X, +} from 'lucide-react'; import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { useHistory, useLocation } from 'react-router-dom'; @@ -24,6 +34,7 @@ enum MenuOption { Agents = 'agents', AddAgent = 'add-agent', CreateJob = 'create-job', + Settings = 'settings', Logout = 'logout', } @@ -31,20 +42,25 @@ export default function NavBar() { const history = useHistory(); const location = useLocation(); const setLogout = useAuth((state) => state.setLogout); + const auth = useAuth((state) => state.auth); const uiContainer = useUIContainer((state) => state.uiContainer); const [isMenuOpened, setMenuOpened] = useState(false); - const isRootPage = ['/inboxes', '/agents'].includes( - location.pathname - ); + const isRootPage = [ + '/inboxes', + '/agents', + '/settings', + '/nodes/connect/method/quick-start', + ].includes(location.pathname); const goBack = () => { history.goBack(); - } + }; const logout = (): void => { setLogout(); }; const onClickMenuOption = (key: MenuOption) => { + console.log('menu option', key, MenuOption.Settings); switch (key) { case MenuOption.Inbox: history.push('/inboxes'); @@ -61,6 +77,9 @@ export default function NavBar() { case MenuOption.AddAgent: history.push('/agents/add'); break; + case MenuOption.Settings: + history.push('/settings'); + break; case MenuOption.Logout: logout(); break; @@ -71,95 +90,109 @@ export default function NavBar() { return ( ); diff --git a/apps/shinkai-visor/src/components/popup/popup.tsx b/apps/shinkai-visor/src/components/popup/popup.tsx index 5b730bbdc..203f6fab5 100644 --- a/apps/shinkai-visor/src/components/popup/popup.tsx +++ b/apps/shinkai-visor/src/components/popup/popup.tsx @@ -15,13 +15,17 @@ import { useGlobalPopupChromeMessage } from '../../hooks/use-global-popup-chrome import { langMessages, locale } from '../../lang/intl'; import { useAuth } from '../../store/auth/auth'; import { AddAgent } from '../add-agent/add-agent'; -import { AddNode } from '../add-node/add-node'; import { Agents } from '../agents/agents'; import { AnimatedRoute } from '../animated-route/animated-routed'; +import { ConnectMethodQrCode } from '../connect-method-qr-code/connec-method-qr-code'; +import { ConnectMethodQuickStart } from '../connect-method-quick-start/connect-method-quick-start'; +import { ConnectMethodRestoreConnection } from '../connect-method-restore-connection/connect-method-restore-connection'; import { CreateInbox } from '../create-inbox/create-inbox'; import { CreateJob } from '../create-job/create-job'; +import { ExportConnection } from '../export-connection/export-connection'; import { Inbox } from '../inbox/inbox'; import { Inboxes } from '../inboxes/inboxes'; +import { Settings } from '../settings/settings'; import { SplashScreen } from '../splash-screen/splash-screen'; import Welcome from '../welcome/welcome'; import { WithNav } from '../with-nav/with-nav'; @@ -31,9 +35,10 @@ export const Popup = () => { const auth = useAuth((state) => state.auth); const location = useLocation(); const [popupVisibility] = useGlobalPopupChromeMessage(); - + useEffect(() => { const isAuthenticated = !!auth; + console.log('isAuth', isAuthenticated, auth); if (isAuthenticated) { ApiConfig.getInstance().setEndpoint(auth.node_address); history.replace('/inboxes'); @@ -42,6 +47,9 @@ export const Popup = () => { history.replace('/welcome'); } }, [history, auth]); + useEffect(() => { + console.log('location', location.pathname); + }, [location]); return ( {popupVisibility && ( @@ -63,18 +71,25 @@ export const Popup = () => { - - - - - - - - - + + + + + + + + + + + + + + + + @@ -101,6 +116,16 @@ export const Popup = () => { + + + + + + + + + + diff --git a/apps/shinkai-visor/src/components/settings/settings.tsx b/apps/shinkai-visor/src/components/settings/settings.tsx new file mode 100644 index 000000000..c6f45f925 --- /dev/null +++ b/apps/shinkai-visor/src/components/settings/settings.tsx @@ -0,0 +1,27 @@ +import { FileKey, SettingsIcon } from 'lucide-react'; +import { FormattedMessage } from 'react-intl'; +import { useHistory } from 'react-router'; + +import { Header } from '../header/header'; +import { Button } from '../ui/button'; + +export const Settings = () => { + const history = useHistory(); + const exportConnection = () => { + history.push('settings/export-connection'); + }; + return ( +
+
} + title={} + /> +
+ +
+
+ ); +}; diff --git a/apps/shinkai-visor/src/components/ui/error-message.tsx b/apps/shinkai-visor/src/components/ui/error-message.tsx index f075ad541..8f4ccaeca 100644 --- a/apps/shinkai-visor/src/components/ui/error-message.tsx +++ b/apps/shinkai-visor/src/components/ui/error-message.tsx @@ -1,8 +1,8 @@ const ErrorMessage = ({ message }: { message: string }) => { return ( -
+
Error: - {message} + {message}
); }; diff --git a/apps/shinkai-visor/src/components/ui/form.tsx b/apps/shinkai-visor/src/components/ui/form.tsx index add95a55e..180e21ba9 100644 --- a/apps/shinkai-visor/src/components/ui/form.tsx +++ b/apps/shinkai-visor/src/components/ui/form.tsx @@ -78,7 +78,7 @@ const FormItem = React.forwardRef< return ( -
+
) }) diff --git a/apps/shinkai-visor/src/components/ui/input.tsx b/apps/shinkai-visor/src/components/ui/input.tsx index b8277252f..3a87dc969 100644 --- a/apps/shinkai-visor/src/components/ui/input.tsx +++ b/apps/shinkai-visor/src/components/ui/input.tsx @@ -9,7 +9,7 @@ const Input = React.forwardRef( return (

-
diff --git a/apps/shinkai-visor/src/components/with-nav/with-nav.tsx b/apps/shinkai-visor/src/components/with-nav/with-nav.tsx index f9adb2430..f2b1dd1be 100644 --- a/apps/shinkai-visor/src/components/with-nav/with-nav.tsx +++ b/apps/shinkai-visor/src/components/with-nav/with-nav.tsx @@ -4,7 +4,7 @@ import NavBar from '../nav/nav'; export const WithNav = (props: PropsWithChildren) => { return ( -
+
{props.children}
diff --git a/apps/shinkai-visor/src/helpers/encryption-keys.ts b/apps/shinkai-visor/src/helpers/encryption-keys.ts new file mode 100644 index 000000000..228e732ef --- /dev/null +++ b/apps/shinkai-visor/src/helpers/encryption-keys.ts @@ -0,0 +1,34 @@ +import { generateEncryptionKeys, generateSignatureKeys } from "@shinkai_network/shinkai-message-ts/utils"; + +export type Encryptionkeys = { + my_device_encryption_pk: string; + my_device_encryption_sk: string; + + my_device_identity_pk: string; + my_device_identity_sk: string; + + profile_encryption_pk: string; + profile_encryption_sk: string; + + profile_identity_pk: string; + profile_identity_sk: string; +}; + +export const generateMyEncryptionKeys = async (): Promise => { + const seed = crypto.getRandomValues(new Uint8Array(32)); + const deviceEncryptionKeys = await generateEncryptionKeys(seed); + const deviceSignataureKeys = await generateSignatureKeys(); + const profileEncryptionKeys = await generateEncryptionKeys(seed); + const profileSignatureKeys = await generateSignatureKeys(); + + return { + my_device_encryption_pk: deviceEncryptionKeys.my_encryption_pk_string, + my_device_encryption_sk: deviceEncryptionKeys.my_encryption_sk_string, + my_device_identity_pk: deviceSignataureKeys.my_identity_pk_string, + my_device_identity_sk: deviceSignataureKeys.my_identity_sk_string, + profile_encryption_pk: profileEncryptionKeys.my_encryption_pk_string, + profile_encryption_sk: profileEncryptionKeys.my_encryption_sk_string, + profile_identity_pk: profileSignatureKeys.my_identity_pk_string, + profile_identity_sk: profileSignatureKeys.my_identity_sk_string, + } +}; \ No newline at end of file diff --git a/apps/shinkai-visor/src/lang/en.json b/apps/shinkai-visor/src/lang/en.json index 9ae90733b..dcae54b72 100644 --- a/apps/shinkai-visor/src/lang/en.json +++ b/apps/shinkai-visor/src/lang/en.json @@ -5,7 +5,7 @@ "inbox.one": "Inbox", "inbox.other": "Inboxes", "setting.one": "Setting", - "settings.other": "Settings", + "setting.other": "Settings", "about": "About Shinkai", "add-node": "Add Node", "registration-code": "Registration Code", @@ -54,5 +54,25 @@ "yesterday": "Yesterday", "installed-sucessfully": "Shinkai Visor was installed sucessfully, navigate to a website and use the action button to start asking Shinkai AI", "file-processing-alert-title": "Your file is being processed", - "file-processing-alert-description": "It can take a few minutes" + "file-processing-alert-description": "It can take a few minutes", + "export-connection": "Export connection", + "generate-connection-file": "Generate connection file", + "passphrase": "Passphrase", + "confirm-passphrase": "Confirm passphrase", + "download": "Download", + "download-keep-safe-place": "Download and keep this connection file in a safe place", + "use-it-to-restore": "Use it with your passphrase to restore the connection to your Shinkai Node", + "passphrases-dont-match": "Passphrases don't match", + "connect-your-shinkai-node": "Connect your Shinkai Node", + "select-connection-method": "Select a method to connect your Shinkai Node", + "quick-connection-connection-method-title": "Quick connection", + "quick-connection-connection-method-description": "Use address to connect for first time", + "qr-code-connection-connection-method-title": "QR Code", + "qr-code-connection-connection-method-description": "Use a QR code to connect", + "restore-connection-connection-method-title": "Restore from connection file", + "restore-connection-connection-method-description": "Use a connection file and passphrase", + "encrypted-connection": "Encrypted connection", + "click-to-upload": "Click to upload or drag and drop", + "restore-connection": "Restore connection", + "did-you-connected-before": "Do you have experience using shinkai?" } diff --git a/apps/shinkai-visor/src/lang/es.json b/apps/shinkai-visor/src/lang/es.json index 87dcb5b12..005ecfec9 100644 --- a/apps/shinkai-visor/src/lang/es.json +++ b/apps/shinkai-visor/src/lang/es.json @@ -5,7 +5,7 @@ "inbox.one": "Bandeja de entrada", "inbox.other": "Bandejas de entrada", "setting.one": "Configuración", - "settings.other": "Configuraciones", + "setting.other": "Configuraciones", "about": "Acerca de Shinkai", "add-node": "Conectar nodo", "registration-code": "Código de registración", @@ -52,5 +52,27 @@ "empty-agents-message": "Conecta tu primer agente para comenzar a preguntar a Shinkai AI", "today": "Hoy", "yesterday": "Ayer", - "installed-sucessfully": "Shinkai Visor se instaló correctamente, navega a un sitio web y usa el botón de acción para comenzar a preguntar a Shinkai AI" + "installed-sucessfully": "Shinkai Visor se instaló correctamente, navega a un sitio web y usa el botón de acción para comenzar a preguntar a Shinkai AI", + "file-processing-alert-title": "Tu archivo está siendo procesado", + "file-processing-alert-description": "Puede tardar unos minutos", + "export-connection": "Exportar conexión", + "generate-connection-file": "Generar archivo de conexión", + "passphrase": "Frase de contraseña", + "confirm-passphrase": "Confirmar frase de contraseña", + "download": "Descargar", + "download-keep-safe-place": "Descarga y guarda este archivo de conexión en un lugar seguro", + "use-it-to-restore": "Úsalo con tu frase de contraseña para restaurar la conexión a tu Nodo Shinkai", + "passphrases-dont-match": "Las frases de contraseña no coinciden", + "connect-your-shinkai-node": "Conecta tu Nodo Shinkai", + "select-connection-method": "Selecciona un método para conectar tu Nodo Shinkai", + "quick-connection-connection-method-title": "Conexión rápida", + "quick-connection-connection-method-description": "Usa la dirección para conectar por primera vez", + "qr-code-connection-connection-method-title": "Código QR", + "qr-code-connection-connection-method-description": "Usa un código QR para conectar", + "restore-connection-connection-method-title": "Restaurar desde archivo de conexión", + "restore-connection-connection-method-description": "Usa un archivo de conexión y frase de contraseña", + "encrypted-connection": "Conexión cifrada", + "click-to-upload": "Haz clic para subir o arrastra y suelta", + "restore-connection": "Restaurar conexión", + "did-you-connected-before": "¿Tienes experiencia usando shinkai?" } diff --git a/libs/shinkai-message-ts/package.json b/libs/shinkai-message-ts/package.json index 6b0772350..3da8c28c1 100644 --- a/libs/shinkai-message-ts/package.json +++ b/libs/shinkai-message-ts/package.json @@ -16,7 +16,8 @@ }, "dependencies": { "@noble/ed25519": "^2.0.0", - "curve25519-js": "^0.0.4" + "curve25519-js": "^0.0.4", + "libsodium-wrappers-sumo": "^0.7.13" }, "exports": { ".": { @@ -38,6 +39,10 @@ "./wasm": { "import": "./wasm.js", "types": "./wasm.d.ts" + }, + "./cryptography": { + "import": "./cryptography.js", + "types": "./cryptography.d.ts" } }, "typesVersions": { @@ -56,6 +61,9 @@ ], "wasm": [ "wasm.d.ts" + ], + "cryptography": [ + "cryptography.d.ts" ] } } diff --git a/libs/shinkai-message-ts/src/cryptography.ts b/libs/shinkai-message-ts/src/cryptography.ts new file mode 100644 index 000000000..84a88e71d --- /dev/null +++ b/libs/shinkai-message-ts/src/cryptography.ts @@ -0,0 +1 @@ +export * from './cryptography/shinkai-encryption'; diff --git a/libs/shinkai-message-ts/src/cryptography/shinkai-encryption.test.ts b/libs/shinkai-message-ts/src/cryptography/shinkai-encryption.test.ts new file mode 100644 index 000000000..20c14147f --- /dev/null +++ b/libs/shinkai-message-ts/src/cryptography/shinkai-encryption.test.ts @@ -0,0 +1,25 @@ +import * as sodium from "libsodium-wrappers-sumo"; + +import { decryptMessageWithPassphrase,encryptMessageWithPassphrase } from './shinkai-encryption'; + +test("encrypt and decrypt message with passphrase", async () => { + await sodium.ready; // Ensure sodium is fully loaded + + const originalMessage = "Hello, world!"; + const passphrase = "my secret passphrase"; + + // Encrypt the message + const encryptedMessage = await encryptMessageWithPassphrase( + originalMessage, + passphrase + ); + + // Decrypt the message + const decryptedMessage = await decryptMessageWithPassphrase( + encryptedMessage, + passphrase + ); + + // The decrypted message should be the same as the original message + expect(decryptedMessage).toBe(originalMessage); +}); \ No newline at end of file diff --git a/libs/shinkai-message-ts/src/cryptography/shinkai-encryption.ts b/libs/shinkai-message-ts/src/cryptography/shinkai-encryption.ts new file mode 100644 index 000000000..3eedbac88 --- /dev/null +++ b/libs/shinkai-message-ts/src/cryptography/shinkai-encryption.ts @@ -0,0 +1,78 @@ +import * as sodium from "libsodium-wrappers-sumo"; + +export async function encryptMessageWithPassphrase( + message: string, + passphrase: string +): Promise { + await sodium.ready; + + const salt = sodium.randombytes_buf(16); // Use a fixed length for the salt + const key = sodium.crypto_pwhash( + 32, + passphrase, + salt, + sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE, + sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE, + sodium.crypto_pwhash_ALG_DEFAULT + ); + + const nonce = sodium.randombytes_buf( + sodium.crypto_aead_chacha20poly1305_IETF_NPUBBYTES + ); + const ciphertext = sodium.crypto_aead_chacha20poly1305_ietf_encrypt( + message, + null, + null, + nonce, + key + ); + + const encrypted_body = + sodium.to_hex(salt) + sodium.to_hex(nonce) + sodium.to_hex(ciphertext); + return `encrypted:${encrypted_body}`; +} + +export async function decryptMessageWithPassphrase( + encryptedBody: string, + passphrase: string +): Promise { + await sodium.ready; + + const parts: string[] = encryptedBody.split(":"); + if (parts[0] !== "encrypted") { + throw new Error("Unexpected variant"); + } + + const content = parts[1]; + const salt = sodium.from_hex(content.slice(0, 32)); // Get the salt from the encrypted message + const key = sodium.crypto_pwhash( + 32, + passphrase, + salt, + sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE, + sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE, + sodium.crypto_pwhash_ALG_DEFAULT + ); + + const nonce = sodium.from_hex(content.slice(32, 56)); + const ciphertext = sodium.from_hex(content.slice(56)); + + try { + const plaintext_bytes = sodium.crypto_aead_chacha20poly1305_ietf_decrypt( + null, + ciphertext, + null, + nonce, + key + ); + const decrypted_body = sodium.to_string(plaintext_bytes); + return decrypted_body; + } catch (e) { + if (e instanceof Error) { + console.error(e.message); + throw new Error("Decryption failure!: " + e.message); + } else { + throw new Error("Decryption failure!"); + } + } +} \ No newline at end of file diff --git a/libs/shinkai-message-ts/src/index.ts b/libs/shinkai-message-ts/src/index.ts index e933299e4..6907042f5 100644 --- a/libs/shinkai-message-ts/src/index.ts +++ b/libs/shinkai-message-ts/src/index.ts @@ -1,6 +1,7 @@ import * as api from "./api"; +import * as cryptography from "./cryptography/shinkai-encryption"; import * as models from "./models"; import * as utils from "./utils"; import * as wasm from "./utils"; -export { api, models, utils, wasm }; +export { api, models, utils, wasm, cryptography }; diff --git a/package-lock.json b/package-lock.json index e0c114b4c..f94c6cb24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,7 @@ "ionicons": "^7.0.0", "jsbi": "^4.3.0", "jspdf": "^2.5.1", + "libsodium-wrappers-sumo": "^0.7.13", "lucide-react": "^0.263.1", "node-forge": ">=1.0.0", "react": "18.2.0", @@ -111,6 +112,7 @@ "@types/chrome": "^0.0.246", "@types/crypto-js": "^4.1.2", "@types/jest": "^29.5.4", + "@types/libsodium-wrappers-sumo": "^0.7.7", "@types/node": "18.14.2", "@types/react": "18.2.14", "@types/react-dom": "18.2.6", @@ -8633,6 +8635,21 @@ "@types/node": "*" } }, + "node_modules/@types/libsodium-wrappers": { + "version": "0.7.12", + "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.12.tgz", + "integrity": "sha512-NNUV6W5KFMYSazUh7bofvIqjHunu1ia24Q4gygXrhluXvvdPtkV6fXuciidYU7eBc9XTltAc6k727wYlrpo9Jg==", + "dev": true + }, + "node_modules/@types/libsodium-wrappers-sumo": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.7.tgz", + "integrity": "sha512-L5KaYOEJqPlMZjP2kUaKjr0vQyv8LRR/QkwAKUazl3JrcEt/VXDdCAi2+Z5mSHOUjan7PEPRSxEPvwsIyXDLDA==", + "dev": true, + "dependencies": { + "@types/libsodium-wrappers": "*" + } + }, "node_modules/@types/linkify-it": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.3.tgz", @@ -17873,6 +17890,19 @@ "node": ">= 0.8.0" } }, + "node_modules/libsodium-sumo": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.13.tgz", + "integrity": "sha512-zTGdLu4b9zSNLfovImpBCbdAA4xkpkZbMnSQjP8HShyOutnGjRHmSOKlsylh1okao6QhLiz7nG98EGn+04cZjQ==" + }, + "node_modules/libsodium-wrappers-sumo": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.13.tgz", + "integrity": "sha512-lz4YdplzDRh6AhnLGF2Dj2IUj94xRN6Bh8T0HLNwzYGwPehQJX6c7iYVrFUPZ3QqxE0bqC+K0IIqqZJYWumwSQ==", + "dependencies": { + "libsodium-sumo": "^0.7.13" + } + }, "node_modules/license-webpack-plugin": { "version": "4.0.2", "dev": true, diff --git a/package.json b/package.json index 2ee5bba5a..fc6eef2bf 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@types/chrome": "^0.0.246", "@types/crypto-js": "^4.1.2", "@types/jest": "^29.5.4", + "@types/libsodium-wrappers-sumo": "^0.7.7", "@types/node": "18.14.2", "@types/react": "18.2.14", "@types/react-dom": "18.2.6", @@ -135,6 +136,7 @@ "ionicons": "^7.0.0", "jsbi": "^4.3.0", "jspdf": "^2.5.1", + "libsodium-wrappers-sumo": "^0.7.13", "lucide-react": "^0.263.1", "node-forge": ">=1.0.0", "react": "18.2.0",