sforkowany z mirror/soapbox
Porównaj commity
17 Commity
Autor | SHA1 | Data |
---|---|---|
Alex Gleason | 0607719c80 | |
Alex Gleason | 43cbdb700f | |
Alex Gleason | 45ef9337d0 | |
Alex Gleason | b9282a20f1 | |
Alex Gleason | 0ec1c73baa | |
Alex Gleason | 7f765e511b | |
Alex Gleason | 583a08ea96 | |
Alex Gleason | 82b61bcf78 | |
Alex Gleason | ad7741847d | |
Alex Gleason | 195edae41f | |
Alex Gleason | ba7a5a80b4 | |
Alex Gleason | 25337f80c5 | |
Alex Gleason | e8433a88ab | |
Alex Gleason | c65239dd15 | |
Alex Gleason | 42eb9e8ddc | |
Alex Gleason | eafa26d8c1 | |
Alex Gleason | 44dd45e27b |
|
@ -1,4 +1,5 @@
|
|||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import { encrypt, createMessage, readKey } from 'openpgp';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
|
@ -7,11 +8,12 @@ import {
|
|||
markChatRead,
|
||||
} from 'soapbox/actions/chats';
|
||||
import { uploadMedia } from 'soapbox/actions/media';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
import { Stack, HStack, IconButton } from 'soapbox/components/ui';
|
||||
import UploadProgress from 'soapbox/components/upload-progress';
|
||||
import UploadButton from 'soapbox/features/compose/components/upload_button';
|
||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||
import { useAppSelector, useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||
import { truncateFilename } from 'soapbox/utils/media';
|
||||
import { initPgpKey, getPgpKey } from 'soapbox/utils/pgp';
|
||||
|
||||
import ChatMessageList from './chat-message-list';
|
||||
|
||||
|
@ -35,6 +37,9 @@ interface IChatBox {
|
|||
const ChatBox: React.FC<IChatBox> = ({ chatId, onSetInputRef, autosize }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const chat = useAppSelector(state => state.chats.items.get(chatId));
|
||||
const account = useOwnAccount();
|
||||
const chatMessageIds = useAppSelector(state => state.chat_message_lists.get(chatId, ImmutableOrderedSet<string>()));
|
||||
|
||||
const [content, setContent] = useState('');
|
||||
|
@ -42,6 +47,7 @@ const ChatBox: React.FC<IChatBox> = ({ chatId, onSetInputRef, autosize }) => {
|
|||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [resetFileKey, setResetFileKey] = useState<number>(fileKeyGen());
|
||||
const [encrypted, setEncrypted] = useState(false);
|
||||
|
||||
const inputElem = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
|
@ -53,9 +59,31 @@ const ChatBox: React.FC<IChatBox> = ({ chatId, onSetInputRef, autosize }) => {
|
|||
setResetFileKey(fileKeyGen());
|
||||
};
|
||||
|
||||
const getParams = () => {
|
||||
const encryptMessage = async() => {
|
||||
if (!account) return null;
|
||||
if (!chat?.account) return null;
|
||||
|
||||
const keys = await getPgpKey(account.fqn) as any;
|
||||
const recipientKeys = await getPgpKey(chat.account) as any;
|
||||
|
||||
const publicKeys = await Promise.all([
|
||||
readKey({ armoredKey: keys.publicKey }),
|
||||
readKey({ armoredKey: recipientKeys.publicKey }),
|
||||
]);
|
||||
|
||||
const privateKey = await readKey({ armoredKey: keys.privateKey });
|
||||
const message = await createMessage({ text: content });
|
||||
|
||||
return await encrypt({
|
||||
message,
|
||||
encryptionKeys: publicKeys,
|
||||
signingKeys: privateKey as any,
|
||||
});
|
||||
};
|
||||
|
||||
const getParams = async() => {
|
||||
return {
|
||||
content,
|
||||
content: encrypted ? await encryptMessage() : content,
|
||||
media_id: attachment && attachment.id,
|
||||
};
|
||||
};
|
||||
|
@ -69,9 +97,9 @@ const ChatBox: React.FC<IChatBox> = ({ chatId, onSetInputRef, autosize }) => {
|
|||
return conds.some(c => c);
|
||||
};
|
||||
|
||||
const sendMessage = () => {
|
||||
const sendMessage = async() => {
|
||||
if (canSubmit() && !isUploading) {
|
||||
const params = getParams();
|
||||
const params = await getParams();
|
||||
|
||||
dispatch(sendChatMessage(chatId, params));
|
||||
clearState();
|
||||
|
@ -152,15 +180,50 @@ const ChatBox: React.FC<IChatBox> = ({ chatId, onSetInputRef, autosize }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const sendPublicKey = async() => {
|
||||
if (!account) return;
|
||||
const { publicKey } = await initPgpKey(account.fqn) as any;
|
||||
|
||||
const params = {
|
||||
content: publicKey,
|
||||
};
|
||||
|
||||
dispatch(sendChatMessage(chatId, params));
|
||||
};
|
||||
|
||||
const handleEncryptionButtonClick = () => {
|
||||
setEncrypted(true);
|
||||
sendPublicKey();
|
||||
};
|
||||
|
||||
const renderEncryptionButton = () => {
|
||||
return (
|
||||
<IconButton
|
||||
className='text-gray-400 hover:text-gray-600'
|
||||
iconClassName='h-5 w-5'
|
||||
src={encrypted ? require('@tabler/icons/icons/lock.svg') : require('@tabler/icons/icons/lock-open.svg')}
|
||||
onClick={handleEncryptionButtonClick}
|
||||
transparent
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderActionButton = () => {
|
||||
return canSubmit() ? (
|
||||
<IconButton
|
||||
className='text-gray-400 hover:text-gray-600'
|
||||
iconClassName='h-5 w-5'
|
||||
src={require('@tabler/icons/icons/send.svg')}
|
||||
title={intl.formatMessage(messages.send)}
|
||||
onClick={sendMessage}
|
||||
transparent
|
||||
/>
|
||||
) : (
|
||||
<UploadButton onSelectFile={handleFiles} resetFileKey={resetFileKey} />
|
||||
<UploadButton
|
||||
iconClassName='h-5 w-5'
|
||||
onSelectFile={handleFiles}
|
||||
resetFileKey={resetFileKey}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -174,9 +237,12 @@ const ChatBox: React.FC<IChatBox> = ({ chatId, onSetInputRef, autosize }) => {
|
|||
<UploadProgress progress={uploadProgress * 100} />
|
||||
)}
|
||||
<div className='chat-box__actions simple_form'>
|
||||
<div className='chat-box__send'>
|
||||
{renderActionButton()}
|
||||
</div>
|
||||
<Stack justifyContent='center' className='absolute right-2.5 inset-y-0'>
|
||||
<HStack>
|
||||
{renderEncryptionButton()}
|
||||
{renderActionButton()}
|
||||
</HStack>
|
||||
</Stack>
|
||||
<textarea
|
||||
rows={1}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
} from 'immutable';
|
||||
import escape from 'lodash/escape';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { readMessage, decrypt, readKey } from 'openpgp';
|
||||
import React, { useState, useEffect, useRef, useLayoutEffect, useMemo } from 'react';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
import { createSelector } from 'reselect';
|
||||
|
@ -13,12 +14,14 @@ import { createSelector } from 'reselect';
|
|||
import { fetchChatMessages, deleteChatMessage } from 'soapbox/actions/chats';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { initReportById } from 'soapbox/actions/reports';
|
||||
import { Text } from 'soapbox/components/ui';
|
||||
import { Text, Icon, HStack } from 'soapbox/components/ui';
|
||||
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
|
||||
import emojify from 'soapbox/features/emoji/emoji';
|
||||
import Bundle from 'soapbox/features/ui/components/bundle';
|
||||
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
|
||||
import { useAppSelector, useAppDispatch, useRefEventHandler } from 'soapbox/hooks';
|
||||
import { useAppSelector, useAppDispatch, useRefEventHandler, useOwnAccount } from 'soapbox/hooks';
|
||||
import { unescapeHTML } from 'soapbox/utils/html';
|
||||
import { isPgpMessage, isPgpEncryptedMessage, getPgpKey } from 'soapbox/utils/pgp';
|
||||
import { onlyEmoji } from 'soapbox/utils/rich_content';
|
||||
|
||||
import type { Menu } from 'soapbox/components/dropdown_menu';
|
||||
|
@ -76,10 +79,12 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chatId, chatMessageIds, a
|
|||
const dispatch = useAppDispatch();
|
||||
|
||||
const me = useAppSelector(state => state.me);
|
||||
const account = useOwnAccount();
|
||||
const chatMessages = useAppSelector(state => getChatMessages(state.chat_messages, chatMessageIds));
|
||||
|
||||
const [initialLoad, setInitialLoad] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [decryptedMessages, setDecryptedMessages] = useState<any>({});
|
||||
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
const messagesEnd = useRef<HTMLDivElement>(null);
|
||||
|
@ -214,7 +219,68 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chatId, chatMessageIds, a
|
|||
};
|
||||
};
|
||||
|
||||
const renderMessage = (chatMessage: ChatMessageEntity) => {
|
||||
const decryptMessage = async(chatMessage: ChatMessageEntity) => {
|
||||
if (!account) return;
|
||||
const content = unescapeHTML(chatMessage.content);
|
||||
|
||||
const keys = await getPgpKey(account.fqn) as any;
|
||||
const recipientKeys = await getPgpKey(chatMessage.account_id) as any;
|
||||
|
||||
const publicKey = await readKey({ armoredKey: recipientKeys.publicKey });
|
||||
const privateKey = await readKey({ armoredKey: keys.privateKey });
|
||||
const message = await readMessage({ armoredMessage: content });
|
||||
|
||||
const { data: decrypted } = await decrypt({
|
||||
message,
|
||||
verificationKeys: publicKey,
|
||||
decryptionKeys: privateKey as any,
|
||||
});
|
||||
|
||||
setDecryptedMessages({ ...decryptedMessages, [chatMessage.id]: decrypted });
|
||||
};
|
||||
|
||||
const renderDecryptedMessage = (chatMessage: ChatMessageEntity) => {
|
||||
const decryptedContent = decryptedMessages[chatMessage.id];
|
||||
|
||||
if (!decryptedContent) {
|
||||
decryptMessage(chatMessage);
|
||||
}
|
||||
|
||||
const newMessage = chatMessage.set('content', decryptedContent || '...');
|
||||
return renderMessage(newMessage, true);
|
||||
};
|
||||
|
||||
const renderEncryptedMessage = (chatMessage: ChatMessageEntity) => {
|
||||
if (isPgpEncryptedMessage(unescapeHTML(chatMessage.content))) {
|
||||
return renderDecryptedMessage(chatMessage);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('chat-message', {
|
||||
'chat-message--me': chatMessage.get('account_id') === me,
|
||||
'chat-message--pending': chatMessage.get('pending', false) === true,
|
||||
})}
|
||||
key={chatMessage.get('id')}
|
||||
>
|
||||
<div
|
||||
title={getFormattedTimestamp(chatMessage)}
|
||||
className='chat-message__bubble'
|
||||
ref={setBubbleRef}
|
||||
tabIndex={0}
|
||||
>
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Icon size={14} src={require('@tabler/icons/icons/info-circle.svg')} />
|
||||
<Text size='xs' className='italic'>
|
||||
Encryption key
|
||||
</Text>
|
||||
</HStack>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMessage = (chatMessage: ChatMessageEntity, encrypted = false) => {
|
||||
const menu: Menu = [
|
||||
{
|
||||
text: intl.formatMessage(messages.delete),
|
||||
|
@ -247,7 +313,13 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chatId, chatMessageIds, a
|
|||
tabIndex={0}
|
||||
>
|
||||
{maybeRenderMedia(chatMessage)}
|
||||
|
||||
<Text size='sm' dangerouslySetInnerHTML={{ __html: parseContent(chatMessage) }} />
|
||||
|
||||
{encrypted && (
|
||||
<Icon size={14} src={require('@tabler/icons/icons/lock.svg')} />
|
||||
)}
|
||||
|
||||
<div className='chat-message__menu'>
|
||||
<DropdownMenuContainer
|
||||
items={menu}
|
||||
|
@ -324,7 +396,12 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chatId, chatMessageIds, a
|
|||
}
|
||||
}
|
||||
|
||||
acc.push(renderMessage(curr));
|
||||
if (isPgpMessage(curr.get('content'))) {
|
||||
acc.push(renderEncryptedMessage(curr));
|
||||
} else {
|
||||
acc.push(renderMessage(curr));
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as React.ReactNode[])}
|
||||
<div style={{ float: 'left', clear: 'both' }} ref={messagesEnd} />
|
||||
|
|
|
@ -20,6 +20,8 @@ interface IUploadButton {
|
|||
onSelectFile: (files: FileList) => void,
|
||||
style?: React.CSSProperties,
|
||||
resetFileKey: number,
|
||||
/** Class name for the <svg> icon. */
|
||||
iconClassName?: string,
|
||||
}
|
||||
|
||||
const UploadButton: React.FC<IUploadButton> = ({
|
||||
|
@ -27,6 +29,7 @@ const UploadButton: React.FC<IUploadButton> = ({
|
|||
unavailable = false,
|
||||
onSelectFile,
|
||||
resetFileKey,
|
||||
iconClassName,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
|
@ -56,6 +59,7 @@ const UploadButton: React.FC<IUploadButton> = ({
|
|||
<IconButton
|
||||
src={src}
|
||||
className='text-gray-400 hover:text-gray-600'
|
||||
iconClassName={iconClassName}
|
||||
title={intl.formatMessage(messages.upload)}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
|
|
|
@ -34,6 +34,7 @@ import ProfilePage from 'soapbox/pages/profile_page';
|
|||
import RemoteInstancePage from 'soapbox/pages/remote_instance_page';
|
||||
import StatusPage from 'soapbox/pages/status_page';
|
||||
import { getAccessToken, getVapidKey } from 'soapbox/utils/auth';
|
||||
import { initPgpKey } from 'soapbox/utils/pgp';
|
||||
import { cacheCurrentUrl } from 'soapbox/utils/redirect';
|
||||
import { isStandalone } from 'soapbox/utils/state';
|
||||
// import GroupSidebarPanel from '../groups/sidebar_panel';
|
||||
|
@ -443,6 +444,8 @@ const UI: React.FC = ({ children }) => {
|
|||
const loadAccountData = () => {
|
||||
if (!account) return;
|
||||
|
||||
initPgpKey(account.fqn);
|
||||
|
||||
dispatch(expandHomeTimeline());
|
||||
|
||||
dispatch(expandNotifications())
|
||||
|
|
|
@ -11,6 +11,8 @@ import {
|
|||
} from 'soapbox/actions/chats';
|
||||
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
|
||||
import { normalizeChatMessage } from 'soapbox/normalizers';
|
||||
import { unescapeHTML } from 'soapbox/utils/html';
|
||||
import { isPgpPublicKeyMessage, savePgpKey } from 'soapbox/utils/pgp';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
|
@ -21,7 +23,15 @@ type APIEntities = Array<APIEntity>;
|
|||
type State = ImmutableMap<string, ChatMessageRecord>;
|
||||
|
||||
const importMessage = (state: State, message: APIEntity) => {
|
||||
return state.set(message.id, normalizeChatMessage(message));
|
||||
const normalMessage = normalizeChatMessage(message);
|
||||
const content = unescapeHTML(normalMessage.content);
|
||||
|
||||
if (isPgpPublicKeyMessage(content)) {
|
||||
// FIXME: use account fqn instead of account_id
|
||||
savePgpKey(normalMessage.account_id, content);
|
||||
}
|
||||
|
||||
return state.set(normalMessage.id, normalMessage);
|
||||
};
|
||||
|
||||
const importMessages = (state: State, messages: APIEntities) =>
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import { isPgpMessage } from '../pgp';
|
||||
|
||||
describe('isPgpMessage()', () => {
|
||||
it('rejects a non-PGP message', () => {
|
||||
expect(isPgpMessage('abcdefg-----BEGIN PGP')).toBe(false);
|
||||
});
|
||||
|
||||
it('identifies a PGP public key', () => {
|
||||
const message = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=LtJq
|
||||
-----END PGP PUBLIC KEY BLOCK-----`;
|
||||
|
||||
expect(isPgpMessage(message)).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
import { generateKey, config } from 'openpgp';
|
||||
|
||||
import KVStore from 'soapbox/storage/kv_store';
|
||||
|
||||
// Configure OpenPGP.js
|
||||
// https://docs.openpgpjs.org/module-config.html
|
||||
config.commentString = 'https://soapbox.pub/#pgp';
|
||||
config.showComment = true;
|
||||
|
||||
/**
|
||||
* Detect whether a message contains valid PGP headers.
|
||||
* @see {@link https://datatracker.ietf.org/doc/html/rfc4880#section-7}
|
||||
*/
|
||||
const isPgpMessage = (message: string): boolean => {
|
||||
return /^-----BEGIN PGP [A-Z ]+-----/.test(message);
|
||||
};
|
||||
|
||||
/** Check whether a message contains a PGP public key. */
|
||||
const isPgpPublicKeyMessage = (message: string): boolean => {
|
||||
return /^-----BEGIN PGP PUBLIC KEY BLOCK-----/.test(message);
|
||||
};
|
||||
|
||||
/** Whether the message is a PGP encrypted message. */
|
||||
const isPgpEncryptedMessage = (message: string): boolean => {
|
||||
return /^-----BEGIN PGP MESSAGE-----/.test(message);
|
||||
};
|
||||
|
||||
/** Generate a key and store it in the browser, if one doesn't already exist. */
|
||||
const initPgpKey = async(fqn: string) => {
|
||||
const item = await KVStore.getItem(`pgp:${fqn}`);
|
||||
|
||||
if (item) {
|
||||
return item;
|
||||
} else {
|
||||
const key = generateKey({ userIDs: [{ name: fqn }] });
|
||||
return await KVStore.setItem(`pgp:${fqn}`, key);
|
||||
}
|
||||
};
|
||||
|
||||
/** Store the public key of another user. */
|
||||
const savePgpKey = async(fqn: string, publicKey: string) => {
|
||||
return await KVStore.setItem(`pgp:${fqn}`, { publicKey });
|
||||
};
|
||||
|
||||
/** Get PGP keys for the given account. */
|
||||
const getPgpKey = async(fqn: string) => {
|
||||
return await KVStore.getItem(`pgp:${fqn}`);
|
||||
};
|
||||
|
||||
export {
|
||||
isPgpMessage,
|
||||
isPgpPublicKeyMessage,
|
||||
isPgpEncryptedMessage,
|
||||
initPgpKey,
|
||||
savePgpKey,
|
||||
getPgpKey,
|
||||
};
|
|
@ -286,20 +286,6 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&__send {
|
||||
.icon-button,
|
||||
button {
|
||||
@apply absolute right-2.5 w-auto h-auto border-0 p-0 m-0 text-black dark:text-white;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
.svg-icon,
|
||||
svg {
|
||||
@apply w-[18px] h-[18px];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 630px) {
|
||||
|
|
|
@ -142,6 +142,7 @@
|
|||
"object-assign": "^4.1.1",
|
||||
"object-fit-images": "^3.2.3",
|
||||
"object.values": "^1.1.0",
|
||||
"openpgp": "^5.3.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-loader": "^7.0.0",
|
||||
|
|
24
yarn.lock
24
yarn.lock
|
@ -3423,6 +3423,16 @@ arrify@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
|
||||
integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
|
||||
|
||||
asn1.js@^5.0.0:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
|
||||
integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
|
||||
dependencies:
|
||||
bn.js "^4.0.0"
|
||||
inherits "^2.0.1"
|
||||
minimalistic-assert "^1.0.0"
|
||||
safer-buffer "^2.1.0"
|
||||
|
||||
ast-types-flow@^0.0.7:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
|
||||
|
@ -3769,6 +3779,11 @@ blurhash@^1.1.5:
|
|||
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.5.tgz#3034104cd5dce5a3e5caa871ae2f0f1f2d0ab566"
|
||||
integrity sha512-a+LO3A2DfxTaTztsmkbLYmUzUeApi0LZuKalwbNmqAHR6HhJGMt1qSV/R3wc+w4DL28holjqO3Bg74aUGavGjg==
|
||||
|
||||
bn.js@^4.0.0:
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
|
||||
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
|
||||
|
||||
body-parser@1.20.0:
|
||||
version "1.20.0"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5"
|
||||
|
@ -8739,6 +8754,13 @@ opener@^1.5.2:
|
|||
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
|
||||
integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
|
||||
|
||||
openpgp@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/openpgp/-/openpgp-5.3.0.tgz#e8fc97e538865b8c095dbd91c7be4203bd1dd1df"
|
||||
integrity sha512-qjCj0vYpV3dmmkE+vURiJ5kVAJwrMk8BPukvpWJiHcTNWKwPVsRS810plIe4klIcHVf1ScgUQwqtBbv99ff+kQ==
|
||||
dependencies:
|
||||
asn1.js "^5.0.0"
|
||||
|
||||
optionator@^0.8.1:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
|
||||
|
@ -10349,7 +10371,7 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0,
|
|||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3":
|
||||
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
|
Ładowanie…
Reference in New Issue