This commit is contained in:
zeppi 2022-09-18 14:12:32 -04:00
parent 9321a0ba37
commit ded2992e44
23 changed files with 1278 additions and 83 deletions

View file

@ -17,6 +17,7 @@ import installDevtools from './installDevtools';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { diskSpaceLinux, diskSpaceWindows, diskSpaceMac } from '../ui/util/diskspace'; import { diskSpaceLinux, diskSpaceWindows, diskSpaceMac } from '../ui/util/diskspace';
import { generateSalt, generateSaltSeed, deriveSecrets, walletHmac } from './sync/sync.js';
const { download } = require('electron-dl'); const { download } = require('electron-dl');
const mime = require('mime'); const mime = require('mime');
@ -326,6 +327,42 @@ ipcMain.on('get-disk-space', async (event) => {
} }
}); });
// Sync cryptography
ipcMain.on('get-salt-seed', () => {
const saltSeed = generateSaltSeed();
rendererWindow.webContents.send('got-salt-seed', saltSeed);
});
ipcMain.on('get-secrets', (event, password, email, saltseed) => {
console.log('password, salt', password, email, saltseed);
const callback = (result) => {
console.log('callback result', result);
rendererWindow.webContents.send('got-secrets', result);
};
deriveSecrets(password, email, saltseed, callback);
});
ipcMain.handle('invoke-get-secrets', (event, password, email, saltseed) => {
return new Promise((resolve, reject) => {
const callback = (err, result) => {
console.log('callback result', result);
if (err) {
return reject(err);
}
return resolve(result);
};
console.log('password, salt', password, email, saltseed);
deriveSecrets(password, email, saltseed, callback);
});
});
ipcMain.handle('invoke-get-salt-seed', (event) => {
return new Promise((resolve, reject) => {
const saltSeed = generateSaltSeed();
return resolve(saltSeed);
});
});
ipcMain.on('version-info-requested', () => { ipcMain.on('version-info-requested', () => {
function formatRc(ver) { function formatRc(ver) {
// Adds dash if needed to make RC suffix SemVer friendly // Adds dash if needed to make RC suffix SemVer friendly
@ -568,3 +605,5 @@ ipcMain.on('upgrade', (event, installerPath) => {
}); });
app.quit(); app.quit();
}); });

View file

@ -0,0 +1,12 @@
{
"name": "test",
"version": "1.0.0",
"description": "",
"main": "testsync.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"type": "module"
}

58
electron/sync/sync.js Normal file
View file

@ -0,0 +1,58 @@
import crypto from 'crypto';
export function generateSalt(email, seed) {
const hashInput = (email + ':' + seed).toString('utf8');
const hash = crypto.createHash('sha256');
hash.update(hashInput);
const hash_output = hash.digest('hex').toString('utf8');
return hash_output;
}
export function generateSaltSeed() {
return crypto.randomBytes(32).toString('hex');
}
export function createHmac(serverWalletState, hmacKey) {
return crypto.randomBytes(32).toString('hex');
}
export function checkHmac(serverWalletState, hmacKey, hmac) {
return crypto.randomBytes(32).toString('hex');
}
export function deriveSecrets(rootPassword, email, saltSeed, callback) {
const encodedPassword = Buffer.from(rootPassword.normalize('NFKC'))
const encodedEmail = Buffer.from(email)
const SCRYPT_N = 1 << 20;
const SCRYPT_R = 8;
const SCRYPT_P = 1;
const KEY_LENGTH = 32;
const NUM_KEYS = 2;
const MAXMEM_MULTIPLIER = 256;
const DEFAULT_MAXMEM = MAXMEM_MULTIPLIER * SCRYPT_N * SCRYPT_R;
function getKeyParts(key) {
const lbryIdPassword = key.slice(0, KEY_LENGTH).toString('base64');
const hmacKey = key.slice(KEY_LENGTH).toString('base64');
return { lbryIdPassword, hmacKey }; // Buffer aa bb cc 6c
}
const salt = generateSalt(encodedEmail, saltSeed);
const scryptCallback = (err, key) => {
if (err) {
// handle error
}
callback(err, getKeyParts(key));
};
crypto.scrypt(encodedPassword, salt, KEY_LENGTH * NUM_KEYS, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P, maxmem: DEFAULT_MAXMEM }, scryptCallback);
}
export function walletHmac(inputString) {
const hmac = crypto.createHmac('sha256', inputString.toString('utf8'));
const res = hmac.digest('hex');
return res;
}

58
electron/sync/testsync.js Normal file
View file

@ -0,0 +1,58 @@
import test from 'tape';
// import sync from '../sync.js';
import { generateSalt, generateSaltSeed, deriveSecrets, walletHmac } from './sync.js';
export default function doTest () {
test('Generate sync seed', (assert) => {
const seed = generateSaltSeed();
console.log('seed', seed);
assert.pass(seed.length === 64);
assert.end();
});
test('Generate salt', (assert) => {
const seed = '80b3aff252588c4097df8f79ad42c8a5cf8d7c9e5339efec8bdb8bc7c5cf25ca';
const email = 'example@example.com';
const salt = generateSalt(email, seed);
console.log('salt', salt);
const expected = 'be6074a96f3ce812ea51b95d48e4b10493e14f6a329df7c0816018c6239661fe';
const actual = salt;
assert.equal(actual, expected,
'salt is expected value.');
assert.end();
});
test('Derive Keys', (assert) => {
const seed = '80b3aff252588c4097df8f79ad42c8a5cf8d7c9e5339efec8bdb8bc7c5cf25ca';
const email = 'example@example.com';
const expectedHmacKey = 'bCxUIryLK0Lf9nKg9yiZDlGleMuGJkadLzTje1PAI+8='; //base64
const expectedLbryIdPassword = 'HKo/J+x4Hsy2NkMvj2JB9RI0yrvEiB4QSA/NHPaT/cA=';
let result;
function cb(e, r) {
console.log('result', r)
assert.equal(r.keys.hmacKey, expectedHmacKey, 'hmac is expected value');
assert.equal(r.keys.lbryIdPassword, expectedLbryIdPassword, 'lbryid password is expected value');
assert.end();
}
deriveSecrets('pass', email, seed, cb);
});
test('CreateHmac', (assert) => {
const hmacKey = 'bCxUIryLK0Lf9nKg9yiZDlGleMuGJkadLzTje1PAI+8=';
const sequence = 1;
const walletState = `zo4MTkyOjE2OjE68QlIU76+W91/v/F1tu8h+kGB0Ee`;
const expectedHmacHex = '52edbad5b0f9d8cf6189795702790cc2cb92060be24672913ab3e4b69c03698b';
const input_str = `${sequence}:${walletState}`;
const hmacHex = walletHmac(input_str);
assert.equal(hmacHex, expectedHmacHex);
assert.end();
});
}
doTest()

View file

@ -192,6 +192,7 @@
"semver": "^5.3.0", "semver": "^5.3.0",
"strip-markdown": "^3.0.3", "strip-markdown": "^3.0.3",
"style-loader": "^0.23.1", "style-loader": "^0.23.1",
"tape": "^5.6.0",
"terser-webpack-plugin": "^4.2.3", "terser-webpack-plugin": "^4.2.3",
"three-full": "^28.0.2", "three-full": "^28.0.2",
"unist-util-visit": "^2.0.3", "unist-util-visit": "^2.0.3",

View file

@ -2318,5 +2318,21 @@
"Odysee Connect --[Section in Help Page]--": "Odysee Connect", "Odysee Connect --[Section in Help Page]--": "Odysee Connect",
"Your hub has blocked this content because it subscribes to the following blocking channel:": "Your hub has blocked this content because it subscribes to the following blocking channel:", "Your hub has blocked this content because it subscribes to the following blocking channel:": "Your hub has blocked this content because it subscribes to the following blocking channel:",
"Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.": "Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.", "Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.": "Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.",
"Creator Comment settings": "Creator Comment settings",
"Remote Sync Settings": "Remote Sync Settings",
"Wallet Sync": "Wallet Sync",
"Manage cross device sync for your wallet": "Manage cross device sync for your wallet",
"With your email and wallet encryption password, we will register you with sync. If your wallet is not currentlyencrypted, it will be after this. Bitch.": "With your email and wallet encryption password, we will register you with sync. If your wallet is not currentlyencrypted, it will be after this. Bitch.",
"Password Again": "Password Again",
"Enter Email": "Enter Email",
"Sign up": "Sign up",
"Let's get you set up. This will require you to encrypt your wallet and remember your password.": "Let's get you set up. This will require you to encrypt your wallet and remember your password.",
"Sign in to your sync account. Or %sign_up%.": "Sign in to your sync account. Or %sign_up%.",
"Sign up for a sync account. Or %sign_in%.": "Sign up for a sync account. Or %sign_in%.",
"Verifying": "Verifying",
"We have sent you an email to verify your account.": "We have sent you an email to verify your account.",
"Doing Math": "Doing Math",
"Hold on, doing some math.": "Hold on, doing some math.",
"You are signed in as %email%.": "You are signed in as %email%.",
"--end--": "--end--" "--end--": "--end--"
} }

View file

@ -46,6 +46,7 @@ import SearchPage from 'page/search';
import SettingsCreatorPage from 'page/settingsCreator'; import SettingsCreatorPage from 'page/settingsCreator';
import SettingsNotificationsPage from 'page/settingsNotifications'; import SettingsNotificationsPage from 'page/settingsNotifications';
import SettingsSyncPage from 'page/settingsSync';
import SettingsPage from 'page/settings'; import SettingsPage from 'page/settings';
import ShowPage from 'page/show'; import ShowPage from 'page/show';
@ -203,7 +204,7 @@ function AppRouter(props: Props) {
{/* Odysee signin */} {/* Odysee signin */}
<Route path={`/$/${PAGES.WELCOME}`} exact component={Welcome} /> <Route path={`/$/${PAGES.WELCOME}`} exact component={Welcome} />
<Route path={`/$/${PAGES.SETTINGS_SYNC}`} exact component={SettingsSyncPage} />
<Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} /> <Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} />
<Route path={`/$/${PAGES.BACKUP}`} exact component={BackupPage} /> <Route path={`/$/${PAGES.BACKUP}`} exact component={BackupPage} />
<Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} /> <Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />

View file

@ -6,6 +6,7 @@ import {
doNotifyDecryptWallet, doNotifyDecryptWallet,
doNotifyEncryptWallet, doNotifyEncryptWallet,
doNotifyForgetPassword, doNotifyForgetPassword,
doOpenModal,
doToggle3PAnalytics, doToggle3PAnalytics,
} from 'redux/actions/app'; } from 'redux/actions/app';
import { doSetDaemonSetting, doClearDaemonSetting, doFindFFmpeg } from 'redux/actions/settings'; import { doSetDaemonSetting, doClearDaemonSetting, doFindFFmpeg } from 'redux/actions/settings';
@ -32,6 +33,7 @@ const perform = (dispatch) => ({
updateWalletStatus: () => dispatch(doWalletStatus()), updateWalletStatus: () => dispatch(doWalletStatus()),
confirmForgetPassword: (modalProps) => dispatch(doNotifyForgetPassword(modalProps)), confirmForgetPassword: (modalProps) => dispatch(doNotifyForgetPassword(modalProps)),
toggle3PAnalytics: (allow) => dispatch(doToggle3PAnalytics(allow)), toggle3PAnalytics: (allow) => dispatch(doToggle3PAnalytics(allow)),
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
}); });
export default connect(select, perform)(SettingSystem); export default connect(select, perform)(SettingSystem);

View file

@ -18,6 +18,8 @@ import { getPasswordFromCookie } from 'util/saved-passwords';
import * as DAEMON_SETTINGS from 'constants/daemon_settings'; import * as DAEMON_SETTINGS from 'constants/daemon_settings';
import SettingEnablePrereleases from 'component/settingEnablePrereleases'; import SettingEnablePrereleases from 'component/settingEnablePrereleases';
import SettingDisableAutoUpdates from 'component/settingDisableAutoUpdates'; import SettingDisableAutoUpdates from 'component/settingDisableAutoUpdates';
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
const IS_MAC = process.platform === 'darwin'; const IS_MAC = process.platform === 'darwin';
@ -148,6 +150,14 @@ export default function SettingSystem(props: Props) {
checked={daemonSettings.save_files} checked={daemonSettings.save_files}
/> />
</SettingsRow> </SettingsRow>
<SettingsRow title={__('Remote Sync Settings')}>
<Button
button="inverse"
label={__('Manage')}
icon={ICONS.ARROW_RIGHT}
navigate={`/$/${PAGES.SETTINGS_SYNC}`}
/>
</SettingsRow>
<SettingsRow <SettingsRow
title={__('Share usage and diagnostic data')} title={__('Share usage and diagnostic data')}
subtitle={ subtitle={
@ -269,24 +279,13 @@ export default function SettingSystem(props: Props) {
title={__('Encrypt my wallet with a custom password')} title={__('Encrypt my wallet with a custom password')}
subtitle={ subtitle={
<React.Fragment> <React.Fragment>
<I18nMessage {__('Secure your local wallet data with a custom password.')}{' '}
tokens={{ <strong>{__('Lost passwords cannot be recovered.')} </strong>
learn_more: ( <Button button="link" label={__('Learn more')} href="https://lbry.com/faq/wallet-encryption" />
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/account-sync" />
),
}}
>
Wallet encryption is currently unavailable until it's supported for synced accounts. It will be
added back soon. %learn_more%.
</I18nMessage>
{/* {__('Secure your local wallet data with a custom password.')}{' '}
<strong>{__('Lost passwords cannot be recovered.')} </strong>
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/wallet-encryption" />. */}
</React.Fragment> </React.Fragment>
} }
> >
<FormField <FormField
disabled
type="checkbox" type="checkbox"
name="encrypt_wallet" name="encrypt_wallet"
onChange={() => onChangeEncryptWallet()} onChange={() => onChangeEncryptWallet()}

View file

@ -505,3 +505,9 @@ export const LSYNC_REGISTER_FAILED = 'LSYNC_REGISTER_FAILED';
export const LSYNC_AUTH_STARTED = 'LSYNC_AUTH_STARTED'; export const LSYNC_AUTH_STARTED = 'LSYNC_AUTH_STARTED';
export const LSYNC_AUTH_COMPLETED = 'LSYNC_AUTH_COMPLETED'; // got token export const LSYNC_AUTH_COMPLETED = 'LSYNC_AUTH_COMPLETED'; // got token
export const LSYNC_AUTH_FAILED = 'LSYNC_AUTH_FAILED'; export const LSYNC_AUTH_FAILED = 'LSYNC_AUTH_FAILED';
export const LSYNC_DERIVE_STARTED = 'LSYNC_DERIVE_STARTED';
export const LSYNC_DERIVE_COMPLETED = 'LSYNC_DERIVE_COMPLETED'; // got secrets
export const LSYNC_DERIVE_FAILED = 'LSYNC_DERIVE_FAILED';
export const LSYNC_GET_SALT_STARTED = 'LSYNC_GET_SALT_STARTED';
export const LSYNC_GET_SALT_COMPLETED = 'LSYNC_GET_SALT_COMPLETED'; // got salt
export const LSYNC_GET_SALT_FAILED = 'LSYNC_GET_SALT_FAILED';

View file

@ -39,3 +39,5 @@ export const COLLECTION_ADD = 'collection_add';
export const COLLECTION_DELETE = 'collection_delete'; export const COLLECTION_DELETE = 'collection_delete';
export const CONFIRM_REMOVE_CARD = 'CONFIRM_REMOVE_CARD'; export const CONFIRM_REMOVE_CARD = 'CONFIRM_REMOVE_CARD';
export const CONFIRM_REMOVE_COMMENT = 'CONFIRM_REMOVE_COMMENT'; export const CONFIRM_REMOVE_COMMENT = 'CONFIRM_REMOVE_COMMENT';
export const SYNC_SIGN_IN = 'SYNC_SIGN_IN';
export const SYNC_SIGN_UP = 'SYNC_SIGN_UP';

View file

@ -46,6 +46,7 @@ export const PAGE_TITLE = {
[PAGES.SETTINGS]: 'Settings', [PAGES.SETTINGS]: 'Settings',
[PAGES.SETTINGS_BLOCKED_MUTED]: 'Block and muted channels', [PAGES.SETTINGS_BLOCKED_MUTED]: 'Block and muted channels',
[PAGES.SETTINGS_CREATOR]: 'Creator settings', [PAGES.SETTINGS_CREATOR]: 'Creator settings',
[PAGES.SETTINGS_SYNC]: 'Sync settings',
[PAGES.SETTINGS_NOTIFICATIONS]: 'Manage notifications', [PAGES.SETTINGS_NOTIFICATIONS]: 'Manage notifications',
[PAGES.SETTINGS_STRIPE_ACCOUNT]: 'Bank Accounts', [PAGES.SETTINGS_STRIPE_ACCOUNT]: 'Bank Accounts',
[PAGES.SETTINGS_STRIPE_CARD]: 'Payment Methods', [PAGES.SETTINGS_STRIPE_CARD]: 'Payment Methods',

View file

@ -46,6 +46,7 @@ exports.SETTINGS_STRIPE_ACCOUNT = 'settings/tip_account';
exports.SETTINGS_NOTIFICATIONS = 'settings/notifications'; exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute'; exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute';
exports.SETTINGS_CREATOR = 'settings/creator'; exports.SETTINGS_CREATOR = 'settings/creator';
exports.SETTINGS_SYNC = 'settings/sync';
exports.SETTINGS_UPDATE_PWD = 'settings/update_password'; exports.SETTINGS_UPDATE_PWD = 'settings/update_password';
exports.SETTINGS_OWN_COMMENTS = 'settings/ownComments'; exports.SETTINGS_OWN_COMMENTS = 'settings/ownComments';
exports.SHOW = 'show'; exports.SHOW = 'show';

View file

@ -1,46 +1,77 @@
// @flow import { ipcRenderer } from 'electron';
/* const BASE_URL = process.env.LBRYSYNC_BASE_URL || 'https://dev.lbry.id';
DeriveSecrets
POST /
*/
import { LBRYSYNC_API as BASE_URL } from 'config';
const SYNC_API_DOWN = 'sync_api_down'; const SYNC_API_DOWN = 'sync_api_down';
const DUPLICATE_EMAIL = 'duplicate_email'; const DUPLICATE_EMAIL = 'duplicate_email';
const UNKNOWN_ERROR = 'unknown_api_error'; const UNKNOWN_ERROR = 'unknown_api_error';
const API_VERSION = 2; const NOT_FOUND = 'not_found';
console.log('process.env.', process.env.LBRYSYNC_BASE_URL);
const API_VERSION = 3;
const POST = 'POST';
const GET = 'GET';
// const API_URL = `${BASE_URL}/api/${API_VERSION}`; // const API_URL = `${BASE_URL}/api/${API_VERSION}`;
const AUTH_ENDPOINT = '/auth/full'; const AUTH_ENDPOINT = '/auth/full';
const REGISTER_ENDPOINT = '/signup'; const REGISTER_ENDPOINT = '/signup';
// const WALLET_ENDPOINT = '/wallet'; const WALLET_ENDPOINT = '/wallet';
const CLIENT_SALT_SEED = '/client-salt-seed';
const Lbrysync = { const Lbrysync = {
apiRequestHeaders: { 'Content-Type': 'application/json' }, apiRequestHeaders: { 'Content-Type': 'application/json' },
apiUrl: `${BASE_URL}/api/${API_VERSION}`, apiUrl: `${BASE_URL}/api/${API_VERSION}`,
setApiHeader: (key: string, value: string) => { setApiHeader: (key, value) => {
Lbrysync.apiRequestHeaders = Object.assign(Lbrysync.apiRequestHeaders, { [key]: value }); Lbrysync.apiRequestHeaders = Object.assign(Lbrysync.apiRequestHeaders, { [key]: value });
}, },
// store state "registered email: email"
register: async (email: string, password: string) => {
try {
const result = await callWithResult(REGISTER_ENDPOINT, { email, password });
return result;
} catch (e) {
return e.message;
}
},
// store state "lbrysynctoken: token"
getAuthToken: async (email: string, password: string, deviceId: string) => {
try {
const result = await callWithResult(AUTH_ENDPOINT, { email, password, deviceId });
return { token: result };
} catch (e) {
return { error: e.message };
}
},
}; };
function callWithResult(endpoint: string, params: ?{} = {}) { export async function fetchSaltSeed(email) {
const buff = Buffer.from(email.toString('utf8'));
const emailParam = buff.toString('base64');
const result = await callWithResult(GET, CLIENT_SALT_SEED, { email: emailParam });
return result;
}
export async function getAuthToken(email, password, deviceId) {
try {
const result = await callWithResult(POST, AUTH_ENDPOINT, { email, password, deviceId });
return { token: result };
} catch (e) {
return { error: e.message };
}
}
export async function register(email, password, saltSeed) {
try {
await callWithResult(POST, REGISTER_ENDPOINT, { email, password, clientSaltSeed: saltSeed });
return;
} catch (e) {
return { error: e.message };
}
}
export async function pushWallet(walletState, hmac, token) {
// token?
const body = {
token: token,
encryptedWallet: walletState.encryptedWallet,
sequence: walletState.sequence,
hmac: hmac,
};
await callWithResult(POST, WALLET_ENDPOINT, { token, hmac, sequence });
}
export async function pullWallet(token) {
try {
await callWithResult(GET, REGISTER_ENDPOINT, { token });
return;
} catch (e) {
return { error: e.message };
}
} // token
function callWithResult(method, endpoint, params = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
apiCall( apiCall(
method,
endpoint, endpoint,
params, params,
(result) => { (result) => {
@ -51,31 +82,40 @@ function callWithResult(endpoint: string, params: ?{} = {}) {
}); });
} }
function apiCall(endpoint: string, params: ?{}, resolve: Function, reject: Function) { function apiCall(method, endpoint, params, resolve, reject) {
const options = { const options = {
method: 'POST', method: method,
body: JSON.stringify(params),
}; };
let searchString = '';
return fetch(`${Lbrysync.apiUrl}${endpoint}`, options) if (method === GET) {
const search = new URLSearchParams(params);
searchString = `?${search}`;
} else if (method === POST) {
options.body = JSON.stringify(params);
}
return fetch(`${Lbrysync.apiUrl}${endpoint}${searchString}`, options)
.then(handleResponse) .then(handleResponse)
.then((response) => { .then((response) => {
return resolve(response.result); return response;
}) })
.catch(reject); .catch(reject);
} }
function handleResponse(response) { function handleResponse(response) {
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300) {
return Promise.resolve(response.json()); return response.json();
} }
if (response.status === 500) { if (response.status === 500) {
return Promise.reject(SYNC_API_DOWN); return Promise.reject(500);
} }
if (response.status === 409) { if (response.status === 409) {
return Promise.reject(DUPLICATE_EMAIL); return Promise.reject(409);
}
if (response.status === 404) {
return Promise.reject(404);
} }
return Promise.reject(UNKNOWN_ERROR); return Promise.reject(UNKNOWN_ERROR);
} }

View file

@ -0,0 +1,61 @@
import { connect } from 'react-redux';
import SettingsSync from './view';
import { selectWalletIsEncrypted } from 'redux/selectors/wallet';
import { doNotifyEncryptWallet, doNotifyDecryptWallet, doNotifyForgetPassword } from 'redux/actions/app';
import {
selectLbrySyncRegistering,
selectLbrySyncEmail,
selectLbrySyncRegisterError,
selectLbrySyncGettingSalt,
selectLbrySyncSaltError,
selectLbrySyncSaltSeed,
selectLbrySyncToken,
selectLbrySyncIsAuthenticating,
selectLbrySyncAuthError,
selectLbrySyncDerivingKeys,
selectLbrySyncEncryptedHmacKey,
selectLbrySyncEncryptedRoot,
selectLbrySyncEncryptedProviderPass,
} from 'redux/selectors/lbrysync';
import {
doLbrysyncGetSalt,
doLbrysyncRegister,
doGenerateSaltSeed,
doDeriveSecrets,
doLbrysyncAuthenticate,
} from 'redux/actions/lbrysync';
const select = (state) => ({
walletEncrypted: selectWalletIsEncrypted(state),
registering: selectLbrySyncRegistering(state),
registeredEmail: selectLbrySyncEmail(state),
registerError: selectLbrySyncRegisterError(state),
gettingSalt: selectLbrySyncGettingSalt(state),
saltError: selectLbrySyncSaltError(state),
saltSeed: selectLbrySyncSaltSeed(state),
token: selectLbrySyncToken(state),
authenticating: selectLbrySyncIsAuthenticating(state),
authError: selectLbrySyncAuthError(state),
derivingKeys: selectLbrySyncDerivingKeys(state),
encHmacKey: selectLbrySyncEncryptedHmacKey(state), // ?
encRootPass: selectLbrySyncEncryptedRoot(state),
encProviderPass: selectLbrySyncEncryptedProviderPass(state),
});
const perform = (dispatch) => ({
encryptWallet: () => dispatch(doNotifyEncryptWallet()),
decryptWallet: () => dispatch(doNotifyDecryptWallet()),
getSalt: (email) => dispatch(doLbrysyncGetSalt(email)),
generateSaltSeed: () => dispatch(doGenerateSaltSeed()),
authenticate: () => dispatch(doLbrysyncAuthenticate()),
deriveSecrets: (p, e, s) => dispatch(doDeriveSecrets(p, e, s)),
register: (email, secrets, saltseed) => dispatch(doLbrysyncRegister(email, secrets, saltseed)),
});
export default connect(select, perform)(SettingsSync);

View file

@ -0,0 +1,296 @@
// @flow
import * as React from 'react';
import Page from 'component/page';
import Card from 'component/common/card';
import Button from 'component/button';
import { Form, FormField } from 'component/common/form';
import I18nMessage from 'component/i18nMessage';
import Spinner from 'component/spinner';
import * as ICONS from 'constants/icons';
type Props = {
walletEncrypted: boolean,
encryptWallet: (string) => void,
decryptWallet: (string) => void,
registering: boolean,
email: string,
registerError: string,
token: string,
authenticating: boolean,
authError: string,
derivingKeys: boolean,
encHmacKey: string, // ?
encRootPass: string,
encProviderPass: string,
getSalt: (string) => void,
gettingSalt: boolean,
saltError: string,
saltSeed: string,
deriveSecrets: (string, string, string) => void, // something
};
export default function NotificationSettingsPage(props: Props) {
// const { } = props;
const {
walletEncrypted,
encryptWallet,
decryptWallet,
registering,
registeredEmail,
registerError,
token,
authenticating,
authError,
authenticate,
derivingKeys,
encHmacKey, // ?
encRootPass,
encProviderPass,
getSalt,
generateSaltSeed,
deriveSecrets,
gettingSalt,
saltError,
saltSeed,
register,
} = props;
const SIGN_IN_MODE = 'sign_in';
const SIGN_UP_MODE = 'sign_up';
const VERIFY_MODE = 'verify';
const MATH_MODE = 'math';
const DONE_MODE = 'done';
const [mode, setMode] = React.useState(registeredEmail ? VERIFY_MODE : SIGN_IN_MODE);
const [email, setEmail] = React.useState();
const [pass, setPass] = React.useState();
const [showPass, setShowPass] = React.useState(false);
const [error, setError] = React.useState('');
React.useEffect(() => {
let interval;
if (!token && registeredEmail) {
interval = setInterval(() => {
console.log('doauthint');
authenticate();
}, 5000);
}
return () => {
clearInterval(interval);
};
}, [registeredEmail, token, authenticate]);
React.useEffect(() => {
if (token && registeredEmail) {
setMode(DONE_MODE);
}
}, [registeredEmail, token, setMode]);
const handleSignUp = async () => {
// get salt for email to make sure
const saltSeedOrError = await getSalt(email);
if (saltSeedOrError.seed) {
setError('Email already registered');
return;
}
// -- if found, report email already exists - sign in?
const saltSeed = await generateSaltSeed();
// saltSeed = generateSaltSeed()
setMode(MATH_MODE);
const secrets = await deriveSecrets(pass, email, saltSeed);
setMode(VERIFY_MODE);
// passwords = driveKeys(root, email, saltSeed);
try {
const registerSuccess = await register(email, secrets, saltSeed);
} catch (e) {
console.log(e);
}
// registerSuccess = register(email, servicePassword, saltSeed)
// poll auth until success
// store [token, rootPassword, providerPass, HmacKey, saltSeed, salt, registeredEmail]
};
const handleSignIn = async () => {
// get saltseed for email
// saltSeed = getSaltSeed()
// -- if error, report email not registered - sign up?
// salt = generateSalt(seed)
// passwords = deriveKeys(root, email, saltSeed);
// token = authenticate(email, servicePassword, deviceId)
// store [token, rootPassword, servicePassword, HmacKey, saltSeed, salt, registeredEmail]
// kick off sync pull
// -- possibly merge conflicts
};
const doneCard = (
<Card
title={__('Done!')}
subtitle={
<I18nMessage
tokens={{
email: email,
}}
>
You are signed in as %email%.
</I18nMessage>
}
actions={<div>Like, done and stuff...</div>}
/>
);
const verifyCard = (
<Card
title={__('Verifying')}
subtitle={__('We have sent you an email to verify your account.')}
actions={<div>Waiting for verification...</div>}
/>
);
const deriveCard = (
<Card title={__('Doing Math')} subtitle={__('Hold on, doing some math.')} actions={<div>Math...</div>} />
);
const signInCard = (
<Card
title={__('Sign In')}
subtitle={
<>
<p>
<I18nMessage
tokens={{
sign_up: <Button button="link" onClick={() => setMode(SIGN_UP_MODE)} label={__('Sign up')} />,
}}
>
Sign in to your sync account. Or %sign_up%.
</I18nMessage>
</p>
</>
}
actions={
<div>
<Form onSubmit={handleSignIn} className="section">
<FormField
autoFocus
placeholder={__('yourstruly@example.com')}
type="email"
name="sign_in_email"
label={__('Email')}
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<FormField
type={showPass ? 'text' : 'password'}
name="root_password"
inputButton={
<>
<Button
icon={showPass ? ICONS.EYE : ICONS.EYE_OFF}
onClick={() => setShowPass(!showPass)}
className={'editable-text__input-button'}
/>
</>
}
label={__('Password Again')}
value={pass}
onChange={(e) => setPass(e.target.value)}
/>
<div className="section__actions">
<Button button="primary" type="submit" label={__('Submit')} disabled={!email} />
</div>
<p className="help--card-actions">
<I18nMessage
tokens={{
terms: <Button button="link" href="https://www.lbry.com/termsofservice" label={__('terms')} />,
}}
>
By creating an account, you agree to our %terms% and confirm you're over the age of 13.
</I18nMessage>
</p>
</Form>
</div>
}
/>
);
const signUpCard = (
<Card
title={__('Sign Up')}
subtitle={
<>
<p>
<I18nMessage
tokens={{
sign_in: <Button button="link" onClick={() => setMode(SIGN_UP_MODE)} label={__('Sign in')} />,
}}
>
Sign up for a sync account. Or %sign_in%.
</I18nMessage>
</p>
</>
}
actions={
<div>
<Form onSubmit={handleSignUp} className="section">
<FormField
autoFocus
placeholder={__('yourstruly@example.com')}
type="email"
name="sign_up_email"
label={__('Email')}
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<FormField
type="password"
name="root_password"
inputButton={
<>
<Button
icon={showPass ? ICONS.EYE : ICONS.EYE_OFF}
onClick={() => setShowPass(!showPass)}
className={'editable-text__input-button'}
/>
</>
}
label={__('Password Again')}
value={pass}
onChange={(e) => setPass(e.target.value)}
/>
<div className="section__actions">
<Button button="primary" type="submit" label={__('Submit')} disabled={!email} />
</div>
<p className="help--card-actions">
<I18nMessage
tokens={{
terms: <Button button="link" href="https://www.lbry.com/termsofservice" label={__('terms')} />,
}}
>
By creating an account, you agree to our %terms% and confirm you're over the age of 13.
</I18nMessage>
</p>
</Form>
</div>
}
/>
);
return (
<Page
noFooter
noSideNavigation
// settingsPage
className="card-stack"
backout={{ title: __('Wallet Sync'), backLabel: __('Back') }}
>
<div className="card-stack">
{mode === DONE_MODE && <>{doneCard}</>}
{mode === SIGN_IN_MODE && <>{signInCard}</>}
{mode === SIGN_UP_MODE && <>{signUpCard}</>}
{mode === MATH_MODE && <>{deriveCard}</>}
{mode === VERIFY_MODE && <>{verifyCard}</>}
</div>
</Page>
);
}

View file

@ -1,20 +1,56 @@
// @flow // @flow
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import Lbrysync from 'lbrysync'; import { ipcRenderer } from 'electron';
import { safeStoreEncrypt, safeStoreDecrypt } from 'util/saved-passwords';
import * as Lbrysync from 'lbrysync';
import Lbry from 'lbry';
import { Lbryio } from "lbryinc";
import { selectSyncHash } from '../selectors/sync';
export const doLbrysyncGetSalt = (email: string) => async (dispatch: Dispatch) => {
const { fetchSaltSeed } = Lbrysync;
dispatch({
type: ACTIONS.LSYNC_GET_SALT_STARTED,
});
try {
const saltOrError = await fetchSaltSeed(email);
dispatch({
type: ACTIONS.LSYNC_GET_SALT_COMPLETED,
data: { email: email, saltSeed: saltOrError},
});
return saltOrError;
} catch (e) {
dispatch({
type: ACTIONS.LSYNC_GET_SALT_FAILED,
data: { email: email, saltError: 'Not Found'},
});
return 'not found';
}
};
// register an email (eventually username) // register an email (eventually username)
export const doLbrysyncRegister = (email: string, password: string) => async (dispatch: Dispatch) => { export const doLbrysyncRegister = (email: string, secrets: any, saltSeed: string) => async (dispatch: Dispatch) => {
const { register } = Lbrysync; const { register } = Lbrysync;
// started // started
dispatch({ dispatch({
type: ACTIONS.LSYNC_REGISTER_STARTED, type: ACTIONS.LSYNC_REGISTER_STARTED,
}); });
const resultIfError = await register(email, password); const resultIfError = await register(email, secrets.providerPass, saltSeed);
const encProviderPass = safeStoreEncrypt(secrets.providerPass);
const encHmacKey = safeStoreEncrypt(secrets.hmacKey);
const enctyptedRoot = safeStoreEncrypt(secrets.rootPassword);
const registerData = {
email,
saltSeed,
providerPass: encProviderPass,
hmacKey: encHmacKey,
rootPass: enctyptedRoot,
};
if (!resultIfError) { if (!resultIfError) {
dispatch({ dispatch({
type: ACTIONS.LSYNC_REGISTER_COMPLETED, type: ACTIONS.LSYNC_REGISTER_COMPLETED,
data: email, data: registerData,
}); });
} else { } else {
dispatch({ dispatch({
@ -26,13 +62,19 @@ export const doLbrysyncRegister = (email: string, password: string) => async (di
// get token given username/password // get token given username/password
export const doLbrysyncAuthenticate = export const doLbrysyncAuthenticate =
(email: string, password: string, deviceId: string) => async (dispatch: Dispatch) => { () => async (dispatch: Dispatch, getState: GetState) => {
const { getAuthToken } = Lbrysync;
// started
dispatch({ dispatch({
type: ACTIONS.LSYNC_AUTH_STARTED, type: ACTIONS.LSYNC_AUTH_STARTED,
}); });
const state = getState();
const { lbrysync } = state;
const { registeredEmail: email, encryptedProviderPass } = lbrysync;
const status = await Lbry.status();
const { installation_id: deviceId } = status;
const password = safeStoreDecrypt(encryptedProviderPass);
const { getAuthToken } = Lbrysync;
const result: { token?: string, error?: string } = await getAuthToken(email, password, deviceId); const result: { token?: string, error?: string } = await getAuthToken(email, password, deviceId);
if (result.token) { if (result.token) {
@ -40,10 +82,221 @@ export const doLbrysyncAuthenticate =
type: ACTIONS.LSYNC_AUTH_COMPLETED, type: ACTIONS.LSYNC_AUTH_COMPLETED,
data: result.token, data: result.token,
}); });
} else if (result.error) { } else {
dispatch({ dispatch({
type: ACTIONS.LSYNC_AUTH_FAILED, type: ACTIONS.LSYNC_AUTH_FAILED,
data: result.error, data: result.error,
}); });
} }
}; };
export const doGenerateSaltSeed = () => async (dispatch: Dispatch) => {
const result = await ipcRenderer.invoke('invoke-get-salt-seed');
return result;
};
export const doDeriveSecrets = (rootPassword: string, email: string, saltSeed: string) => async (dispatch: Dispatch) =>
{
dispatch({
type: ACTIONS.LSYNC_DERIVE_STARTED,
});
try {
const result = await ipcRenderer.invoke('invoke-get-secrets', rootPassword, email, saltSeed);
const data = {
hmacKey: result.hmacKey,
rootPassword,
providerPass: result.lbryIdPassword,
};
dispatch({
type: ACTIONS.LSYNC_DERIVE_COMPLETED,
data,
});
return data;
} catch (e) {
dispatch({
type: ACTIONS.LSYNC_DERIVE_FAILED,
data: {
error: e,
},
});
return { error: e.message };
}
};
export async function doSetSync() {
return (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const localHash = selectSyncHash(state);
const { lbrysync } = state;
const { authToken, encryptedRoot } = lbrysync;
dispatch({
type: ACTIONS.SET_SYNC_STARTED,
});
let error;
try {
const status = Lbry.wallet_status();
if (status.is_locked) {
throw new Error('Error parsing i18n messages file: ' + messagesFilePath + ' err: ' + err);
}
} catch(e) {
error = e.message;
}
if (!error) {
const syncData = await Lbry.sync_apply({ password: , data: response.data, blocking: true })
}
// return Lbryio.call('sync', 'set', { old_hash: oldHash, new_hash: newHash, data }, 'post')
return pushWallet(authToken)
.then((response) => {
if (!response.hash) {
throw Error('No hash returned for sync/set.');
}
return dispatch({
type: ACTIONS.SET_SYNC_COMPLETED,
data: { syncHash: response.hash },
});
})
.catch((error) => {
dispatch({
type: ACTIONS.SET_SYNC_FAILED,
data: { error },
});
});
};
}
export function doGetSync(passedPassword?: string, callback?: (any, ?boolean) => void) {
const password = passedPassword === null || passedPassword === undefined ? '' : passedPassword;
function handleCallback(error, hasNewData) {
if (callback) {
if (typeof callback !== 'function') {
throw new Error('Second argument passed to "doGetSync" must be a function');
}
callback(error, hasNewData);
}
}
return (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const localHash = selectSyncHash(state);
const { lbrysync } = state;
const { authToken, encryptedRoot } = lbrysync;
const { pullWallet } = Lbrysync;
dispatch({
type: ACTIONS.GET_SYNC_STARTED,
});
const data = {};
Lbry.wallet_status()
.then((status) => {
if (status.is_locked) {
return Lbry.wallet_unlock({ password });
}
// Wallet is already unlocked
return true;
})
.then((isUnlocked) => {
if (isUnlocked) {
return Lbry.sync_hash(); //unnec
}
data.unlockFailed = true;
throw new Error();
})
// .then((hash?: string) => Lbryio.call('sync', 'get', { hash }, 'post'))
.then((hash?: string) => pullWallet(authToken))
.then((response: any) => {
// get data, put it in sync apply.
const syncHash = response.hash;
data.syncHash = syncHash;
data.syncData = response.data;
data.changed = response.changed || syncHash !== localHash;
data.hasSyncedWallet = true;
if (response.changed) {
return Lbry.sync_apply({ password, data: response.data, blocking: true });
}
})
.then((response) => {
if (!response) {
dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data });
handleCallback(null, data.changed);
return;
}
const { hash: walletHash, data: walletData } = response;
if (walletHash !== data.syncHash) {
// different local hash, need to synchronise
dispatch(doSetSync(data.syncHash, walletHash, walletData));
}
dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data });
handleCallback(null, data.changed);
})
.catch((syncAttemptError) => {
const badPasswordError =
syncAttemptError && syncAttemptError.data && syncAttemptError.data.name === BAD_PASSWORD_ERROR_NAME;
if (data.unlockFailed) {
dispatch({ type: ACTIONS.GET_SYNC_FAILED, data: { error: syncAttemptError } });
if (badPasswordError) {
dispatch({ type: ACTIONS.SYNC_APPLY_BAD_PASSWORD });
}
handleCallback(syncAttemptError);
} else if (data.hasSyncedWallet) {
const error = (syncAttemptError && syncAttemptError.message) || 'Error getting synced wallet';
dispatch({
type: ACTIONS.GET_SYNC_FAILED,
data: {
error,
},
});
if (badPasswordError) {
dispatch({ type: ACTIONS.SYNC_APPLY_BAD_PASSWORD });
}
handleCallback(error);
} else {
const noWalletError = syncAttemptError && syncAttemptError.message === NO_WALLET_ERROR;
dispatch({
type: ACTIONS.GET_SYNC_COMPLETED,
data: {
hasSyncedWallet: false,
syncHash: null,
// If there was some unknown error, bail
fatalError: !noWalletError,
},
});
// user doesn't have a synced wallet
// call sync_apply to get data to sync
// first time sync. use any string for old hash
if (noWalletError) {
Lbry.sync_apply({ password })
.then(({ hash: walletHash, data: syncApplyData }) => {
dispatch(doSetSync('', walletHash, syncApplyData));
handleCallback();
})
.catch((syncApplyError) => {
handleCallback(syncApplyError);
});
}
}
});
};
}

View file

@ -104,7 +104,7 @@ export const doGetSyncDesktop =
const getSyncPending = selectGetSyncIsPending(state); const getSyncPending = selectGetSyncIsPending(state);
const setSyncPending = selectSetSyncIsPending(state); const setSyncPending = selectSetSyncIsPending(state);
const syncLocked = selectSyncIsLocked(state); const syncLocked = selectSyncIsLocked(state);
// here we instead do the new getsync with the derived password
return getSavedPassword().then((savedPassword) => { return getSavedPassword().then((savedPassword) => {
const passwordArgument = password || password === '' ? password : savedPassword === null ? '' : savedPassword; const passwordArgument = password || password === '' ? password : savedPassword === null ? '' : savedPassword;

View file

@ -2,13 +2,24 @@ import * as ACTIONS from 'constants/action_types';
import { handleActions } from 'util/redux-utils'; import { handleActions } from 'util/redux-utils';
const defaultState = { const defaultState = {
syncProvider: null,
// reg
registering: false, registering: false,
registeredEmail: null, registeredEmail: null,
registerError: null, registerError: null,
syncProvider: null, // authtoken
isAuthenticating: false, isAuthenticating: false,
authError: null, authError: null,
authToken: null, // store this elsewhere? authToken: null, // store this elsewhere
// keys
derivingKeys: false,
encryptedHmacKey: null,
encryptedRoot: null,
encryptedProviderPass: null,
// salt
gettingSalt: false,
saltSeed: null,
saltError: null,
}; };
export const lbrysyncReducer = handleActions( export const lbrysyncReducer = handleActions(
@ -17,20 +28,27 @@ export const lbrysyncReducer = handleActions(
[ACTIONS.LSYNC_REGISTER_STARTED]: (state) => ({ [ACTIONS.LSYNC_REGISTER_STARTED]: (state) => ({
...state, ...state,
registering: true, registering: true,
registerError: null,
}), }),
[ACTIONS.LSYNC_REGISTER_COMPLETED]: (state, action) => ({ [ACTIONS.LSYNC_REGISTER_COMPLETED]: (state, action) => ({
...state, ...state,
registeredEmail: action.data, registeredEmail: action.data.email,
encryptedHmacKey: action.data.hmacKey,
encryptedProviderPass: action.data.providerPass,
encryptedRoot: action.data.rootPass,
saltSeed: action.data.saltSeed,
}), }),
[ACTIONS.LSYNC_REGISTER_FAILED]: (state) => ({ [ACTIONS.LSYNC_REGISTER_FAILED]: (state, action) => ({
...state, ...state,
registeredEmail: null, registeredEmail: null,
registering: false, registering: false,
registerError: action.data.error,
}), }),
// Auth // Auth
[ACTIONS.LSYNC_AUTH_STARTED]: (state) => ({ [ACTIONS.LSYNC_AUTH_STARTED]: (state) => ({
...state, ...state,
isAuthenticating: true, isAuthenticating: true,
authError: null,
}), }),
[ACTIONS.LSYNC_AUTH_COMPLETED]: (state, action) => ({ [ACTIONS.LSYNC_AUTH_COMPLETED]: (state, action) => ({
...state, ...state,
@ -41,7 +59,36 @@ export const lbrysyncReducer = handleActions(
authError: action.data, authError: action.data,
isAuthenticating: false, isAuthenticating: false,
}), }),
// ... // derive
[ACTIONS.LSYNC_DERIVE_STARTED]: (state) => ({
...state,
derivingKeys: true,
deriveError: null,
}),
[ACTIONS.LSYNC_DERIVE_COMPLETED]: (state, action) => ({
...state,
derivingKeys: false,
}),
[ACTIONS.LSYNC_DERIVE_FAILED]: (state, action) => ({
...state,
deriveError: action.data.error,
derivingKeys: false,
}),
// salt
[ACTIONS.LSYNC_GET_SALT_STARTED]: (state) => ({
...state,
gettingSalt: true,
saltError: null,
}),
[ACTIONS.LSYNC_GET_SALT_COMPLETED]: (state, action) => ({
...state,
gettingSalt: false,
}),
[ACTIONS.LSYNC_GET_SALT_FAILED]: (state, action) => ({
...state,
saltError: action.data.error,
gettingSalt: false,
}),
}, },
defaultState defaultState
); );

View file

@ -3,14 +3,19 @@ import { createSelector } from 'reselect';
const selectState = (state) => state.lbrysync || {}; const selectState = (state) => state.lbrysync || {};
export const selectLbrySyncRegistering = createSelector(selectState, (state) => state.registering); export const selectLbrySyncRegistering = createSelector(selectState, (state) => state.registering);
export const selectLbrySyncEmail = createSelector(selectState, (state) => state.registeredEmail); export const selectLbrySyncEmail = createSelector(selectState, (state) => state.registeredEmail);
export const selectLbrySyncRegisterError = createSelector(selectState, (state) => state.registerError); export const selectLbrySyncRegisterError = createSelector(selectState, (state) => state.registerError);
// probably shouldn't store this here. export const selectLbrySyncGettingSalt = createSelector(selectState, (state) => state.gettingSalt);
export const selectLbrySyncToken = createSelector(selectState, (state) => state.registering); export const selectLbrySyncSaltError = createSelector(selectState, (state) => state.saltError);
export const selectLbrySyncSaltSeed = createSelector(selectState, (state) => state.saltSeed);
export const selectLbrySyncIsAuthenticating = createSelector(selectState, (state) => state.isAuthenticating); export const selectLbrySyncIsAuthenticating = createSelector(selectState, (state) => state.isAuthenticating);
export const selectLbrySyncAuthError = createSelector(selectState, (state) => state.authError); export const selectLbrySyncAuthError = createSelector(selectState, (state) => state.authError);
export const selectLbrySyncToken = createSelector(selectState, (state) => state.authToken);
export const selectLbrySyncDerivingKeys = createSelector(selectState, (state) => state.derivingKeys);
export const selectLbrySyncEncryptedHmacKey = createSelector(selectState, (state) => state.encryptedHmacKey);
export const selectLbrySyncEncryptedRoot = createSelector(selectState, (state) => state.encryptedRoot);
export const selectLbrySyncEncryptedProviderPass = createSelector(selectState, (state) => state.encryptedProviderPass);

View file

@ -1,3 +1,5 @@
const { safeStorage } = require('@electron/remote');
const { DOMAIN } = require('../../config.js'); const { DOMAIN } = require('../../config.js');
const AUTH_TOKEN = 'auth_token'; const AUTH_TOKEN = 'auth_token';
const SAVED_PASSWORD = 'saved_password'; const SAVED_PASSWORD = 'saved_password';
@ -127,6 +129,17 @@ function doAuthTokenRefresh() {
} }
} }
function safeStoreEncrypt(ssVal) {
const buffer = safeStorage.encryptString(ssVal);
console.log('buffer', buffer.toString('base64'));
return buffer.toString('base64');
}
function safeStoreDecrypt(ssVal) {
const buffer = safeStorage.decryptString(Buffer.from(ssVal, 'base64'));
return buffer.toString();
}
module.exports = { module.exports = {
setSavedPassword, setSavedPassword,
getSavedPassword, getSavedPassword,
@ -137,4 +150,6 @@ module.exports = {
deleteAuthToken, deleteAuthToken,
doSignOutCleanup, doSignOutCleanup,
doAuthTokenRefresh, doAuthTokenRefresh,
safeStoreEncrypt,
safeStoreDecrypt,
}; };

View file

@ -0,0 +1,32 @@
export const makeMergedPrefs = (alt, base) => {
let finalPrefs = base;
let baseData = base.value;
let altData = alt.value;
if (!altData) {
return base;
}
let mergedBlockListSet = new Set(baseData.blocked || []);
let mergedSubscriptionsSet = new Set(baseData.subscriptions || []);
let mergedTagsSet = new Set(baseData.tags || []);
const altBlocklist = altData.blocked || [];
const altSubscriptions = altData.subscriptions || [];
const altTags = altData.tags || [];
if (altBlocklist.length) {
altBlocklist.forEach((el) => mergedBlockListSet.add(el));
}
if (altSubscriptions.length) {
altSubscriptions.forEach((el) => mergedSubscriptionsSet.add(el));
}
if (altTags.length) {
altTags.forEach((el) => mergedTagsSet.add(el));
}
baseData.blocked = Array.from(mergedBlockListSet);
baseData.subscriptions = Array.from(mergedSubscriptionsSet);
baseData.tags = Array.from(mergedTagsSet);
finalPrefs.value = baseData;
return finalPrefs;
};

274
yarn.lock
View file

@ -3801,6 +3801,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"array.prototype.every@npm:^1.1.3":
version: 1.1.3
resolution: "array.prototype.every@npm:1.1.3"
dependencies:
call-bind: ^1.0.2
define-properties: ^1.1.3
es-abstract: ^1.19.0
is-string: ^1.0.7
checksum: bbcc864ac1271307043a16262455a6f917d183060a7e5b99c7c710ee611d40c1065f4ec674323b50cf8b987f2d0c9ca9e9ff9cbf4bcc7740f82e731ec2a58d6f
languageName: node
linkType: hard
"array.prototype.flat@npm:^1.2.5": "array.prototype.flat@npm:^1.2.5":
version: 1.3.0 version: 1.3.0
resolution: "array.prototype.flat@npm:1.3.0" resolution: "array.prototype.flat@npm:1.3.0"
@ -3995,6 +4007,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"available-typed-arrays@npm:^1.0.5":
version: 1.0.5
resolution: "available-typed-arrays@npm:1.0.5"
checksum: 20eb47b3cefd7db027b9bbb993c658abd36d4edd3fe1060e83699a03ee275b0c9b216cc076ff3f2db29073225fb70e7613987af14269ac1fe2a19803ccc97f1a
languageName: node
linkType: hard
"aws-sign2@npm:~0.7.0": "aws-sign2@npm:~0.7.0":
version: 0.7.0 version: 0.7.0
resolution: "aws-sign2@npm:0.7.0" resolution: "aws-sign2@npm:0.7.0"
@ -6471,6 +6490,29 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"deep-equal@npm:^2.0.5":
version: 2.0.5
resolution: "deep-equal@npm:2.0.5"
dependencies:
call-bind: ^1.0.0
es-get-iterator: ^1.1.1
get-intrinsic: ^1.0.1
is-arguments: ^1.0.4
is-date-object: ^1.0.2
is-regex: ^1.1.1
isarray: ^2.0.5
object-is: ^1.1.4
object-keys: ^1.1.1
object.assign: ^4.1.2
regexp.prototype.flags: ^1.3.0
side-channel: ^1.0.3
which-boxed-primitive: ^1.0.1
which-collection: ^1.0.1
which-typed-array: ^1.1.2
checksum: 2bb7332badf589b540184d25098acac750e30fe11c8dce4523d03fc5db15f46881a0105e6bf0b64bb0c57213a95ed964029ff0259026ad6f7f9e0019f8200de5
languageName: node
linkType: hard
"deep-extend@npm:^0.6.0": "deep-extend@npm:^0.6.0":
version: 0.6.0 version: 0.6.0
resolution: "deep-extend@npm:0.6.0" resolution: "deep-extend@npm:0.6.0"
@ -6547,6 +6589,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"defined@npm:^1.0.0":
version: 1.0.0
resolution: "defined@npm:1.0.0"
checksum: 77672997c5001773371c4dbcce98da0b3dc43089d6da2ad87c4b800adb727633cea8723ea3889fe0c2112a2404e2fd07e3bfd0e55f7426aa6441d8992045dbd5
languageName: node
linkType: hard
"del@npm:^3.0.0": "del@npm:^3.0.0":
version: 3.0.0 version: 3.0.0
resolution: "del@npm:3.0.0" resolution: "del@npm:3.0.0"
@ -6973,6 +7022,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"dotignore@npm:^0.1.2":
version: 0.1.2
resolution: "dotignore@npm:0.1.2"
dependencies:
minimatch: ^3.0.4
bin:
ignored: bin/ignored
checksum: 06bab15e2a2400c6f823a0edbcd73661180f6245a4041a3fe3b9fde4b22ae74b896604df4520a877093f05c656bd080087376c9f605bccdea847664c59910f37
languageName: node
linkType: hard
"duck@npm:^0.1.12": "duck@npm:^0.1.12":
version: 0.1.12 version: 0.1.12
resolution: "duck@npm:0.1.12" resolution: "duck@npm:0.1.12"
@ -7475,7 +7535,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"es-abstract@npm:^1.17.2, es-abstract@npm:^1.19.0, es-abstract@npm:^1.19.1, es-abstract@npm:^1.19.2, es-abstract@npm:^1.19.5, es-abstract@npm:^1.20.1": "es-abstract@npm:^1.17.2, es-abstract@npm:^1.19.0, es-abstract@npm:^1.19.1, es-abstract@npm:^1.19.2, es-abstract@npm:^1.19.5, es-abstract@npm:^1.20.0, es-abstract@npm:^1.20.1":
version: 1.20.1 version: 1.20.1
resolution: "es-abstract@npm:1.20.1" resolution: "es-abstract@npm:1.20.1"
dependencies: dependencies:
@ -7513,6 +7573,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"es-get-iterator@npm:^1.1.1":
version: 1.1.2
resolution: "es-get-iterator@npm:1.1.2"
dependencies:
call-bind: ^1.0.2
get-intrinsic: ^1.1.0
has-symbols: ^1.0.1
is-arguments: ^1.1.0
is-map: ^2.0.2
is-set: ^2.0.2
is-string: ^1.0.5
isarray: ^2.0.5
checksum: f75e66acb6a45686fa08b3ade9c9421a70d36a0c43ed4363e67f4d7aab2226cb73dd977cb48abbaf75721b946d3cd810682fcf310c7ad0867802fbf929b17dcf
languageName: node
linkType: hard
"es-shim-unscopables@npm:^1.0.0": "es-shim-unscopables@npm:^1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "es-shim-unscopables@npm:1.0.0" resolution: "es-shim-unscopables@npm:1.0.0"
@ -8607,6 +8683,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"for-each@npm:^0.3.3":
version: 0.3.3
resolution: "for-each@npm:0.3.3"
dependencies:
is-callable: ^1.1.3
checksum: 6c48ff2bc63362319c65e2edca4a8e1e3483a2fabc72fbe7feaf8c73db94fc7861bd53bc02c8a66a0c1dd709da6b04eec42e0abdd6b40ce47305ae92a25e5d28
languageName: node
linkType: hard
"for-in@npm:^1.0.2": "for-in@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "for-in@npm:1.0.2" resolution: "for-in@npm:1.0.2"
@ -8884,7 +8969,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.0, get-intrinsic@npm:^1.1.1": "get-intrinsic@npm:^1.0.1, get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.0, get-intrinsic@npm:^1.1.1":
version: 1.1.2 version: 1.1.2
resolution: "get-intrinsic@npm:1.1.2" resolution: "get-intrinsic@npm:1.1.2"
dependencies: dependencies:
@ -8902,6 +8987,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"get-package-type@npm:^0.1.0":
version: 0.1.0
resolution: "get-package-type@npm:0.1.0"
checksum: bba0811116d11e56d702682ddef7c73ba3481f114590e705fc549f4d868972263896af313c57a25c076e3c0d567e11d919a64ba1b30c879be985fc9d44f96148
languageName: node
linkType: hard
"get-stdin@npm:^5.0.1": "get-stdin@npm:^5.0.1":
version: 5.0.1 version: 5.0.1
resolution: "get-stdin@npm:5.0.1" resolution: "get-stdin@npm:5.0.1"
@ -9303,6 +9395,23 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"has-dynamic-import@npm:^2.0.1":
version: 2.0.1
resolution: "has-dynamic-import@npm:2.0.1"
dependencies:
call-bind: ^1.0.2
get-intrinsic: ^1.1.1
checksum: 1cb60255cdd354a5f53997dd4c8ae0f821706ced3d1047bb810cb74400f28988b08d4d986318cb6610b79e6b9993a6592e678b6cef3ef0b71ab553eaa99b9c4d
languageName: node
linkType: hard
"has-flag@npm:^2.0.0":
version: 2.0.0
resolution: "has-flag@npm:2.0.0"
checksum: 7d060d142ef6740c79991cb99afe5962b267e6e95538bf8b607026b9b1e7451288927bc8e7b4a9484a8b99935c0af023070f91ee49faef791ecd401dc58b2e8d
languageName: node
linkType: hard
"has-flag@npm:^3.0.0": "has-flag@npm:^3.0.0":
version: 3.0.0 version: 3.0.0
resolution: "has-flag@npm:3.0.0" resolution: "has-flag@npm:3.0.0"
@ -10246,7 +10355,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-arguments@npm:^1.0.4": "is-arguments@npm:^1.0.4, is-arguments@npm:^1.1.0":
version: 1.1.1 version: 1.1.1
resolution: "is-arguments@npm:1.1.1" resolution: "is-arguments@npm:1.1.1"
dependencies: dependencies:
@ -10314,7 +10423,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-callable@npm:^1.1.4, is-callable@npm:^1.2.4": "is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.4":
version: 1.2.4 version: 1.2.4
resolution: "is-callable@npm:1.2.4" resolution: "is-callable@npm:1.2.4"
checksum: 1a28d57dc435797dae04b173b65d6d1e77d4f16276e9eff973f994eadcfdc30a017e6a597f092752a083c1103cceb56c91e3dadc6692fedb9898dfaba701575f checksum: 1a28d57dc435797dae04b173b65d6d1e77d4f16276e9eff973f994eadcfdc30a017e6a597f092752a083c1103cceb56c91e3dadc6692fedb9898dfaba701575f
@ -10395,7 +10504,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-date-object@npm:^1.0.1": "is-date-object@npm:^1.0.1, is-date-object@npm:^1.0.2":
version: 1.0.5 version: 1.0.5
resolution: "is-date-object@npm:1.0.5" resolution: "is-date-object@npm:1.0.5"
dependencies: dependencies:
@ -10579,6 +10688,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-map@npm:^2.0.1, is-map@npm:^2.0.2":
version: 2.0.2
resolution: "is-map@npm:2.0.2"
checksum: ace3d0ecd667bbdefdb1852de601268f67f2db725624b1958f279316e13fecb8fa7df91fd60f690d7417b4ec180712f5a7ee967008e27c65cfd475cc84337728
languageName: node
linkType: hard
"is-natural-number@npm:^4.0.1": "is-natural-number@npm:^4.0.1":
version: 4.0.1 version: 4.0.1
resolution: "is-natural-number@npm:4.0.1" resolution: "is-natural-number@npm:4.0.1"
@ -10749,7 +10865,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-regex@npm:^1.0.4, is-regex@npm:^1.1.4": "is-regex@npm:^1.0.4, is-regex@npm:^1.1.1, is-regex@npm:^1.1.4":
version: 1.1.4 version: 1.1.4
resolution: "is-regex@npm:1.1.4" resolution: "is-regex@npm:1.1.4"
dependencies: dependencies:
@ -10780,6 +10896,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-set@npm:^2.0.1, is-set@npm:^2.0.2":
version: 2.0.2
resolution: "is-set@npm:2.0.2"
checksum: b64343faf45e9387b97a6fd32be632ee7b269bd8183701f3b3f5b71a7cf00d04450ed8669d0bd08753e08b968beda96fca73a10fd0ff56a32603f64deba55a57
languageName: node
linkType: hard
"is-shared-array-buffer@npm:^1.0.2": "is-shared-array-buffer@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "is-shared-array-buffer@npm:1.0.2" resolution: "is-shared-array-buffer@npm:1.0.2"
@ -10821,6 +10944,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-typed-array@npm:^1.1.9":
version: 1.1.9
resolution: "is-typed-array@npm:1.1.9"
dependencies:
available-typed-arrays: ^1.0.5
call-bind: ^1.0.2
es-abstract: ^1.20.0
for-each: ^0.3.3
has-tostringtag: ^1.0.0
checksum: 11910f1e58755fef43bf0074e52fa5b932bf101ec65d613e0a83d40e8e4c6e3f2ee142d624ebc7624c091d3bbe921131f8db7d36ecbbb71909f2fe310c1faa65
languageName: node
linkType: hard
"is-typedarray@npm:^1.0.0, is-typedarray@npm:~1.0.0": "is-typedarray@npm:^1.0.0, is-typedarray@npm:~1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "is-typedarray@npm:1.0.0" resolution: "is-typedarray@npm:1.0.0"
@ -10837,6 +10973,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-weakmap@npm:^2.0.1":
version: 2.0.1
resolution: "is-weakmap@npm:2.0.1"
checksum: 1222bb7e90c32bdb949226e66d26cb7bce12e1e28e3e1b40bfa6b390ba3e08192a8664a703dff2a00a84825f4e022f9cd58c4599ff9981ab72b1d69479f4f7f6
languageName: node
linkType: hard
"is-weakref@npm:^1.0.2": "is-weakref@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "is-weakref@npm:1.0.2" resolution: "is-weakref@npm:1.0.2"
@ -10846,6 +10989,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-weakset@npm:^2.0.1":
version: 2.0.2
resolution: "is-weakset@npm:2.0.2"
dependencies:
call-bind: ^1.0.2
get-intrinsic: ^1.1.1
checksum: 5d8698d1fa599a0635d7ca85be9c26d547b317ed8fd83fc75f03efbe75d50001b5eececb1e9971de85fcde84f69ae6f8346bc92d20d55d46201d328e4c74a367
languageName: node
linkType: hard
"is-whitespace-character@npm:^1.0.0": "is-whitespace-character@npm:^1.0.0":
version: 1.0.4 version: 1.0.4
resolution: "is-whitespace-character@npm:1.0.4" resolution: "is-whitespace-character@npm:1.0.4"
@ -10904,6 +11057,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"isarray@npm:^2.0.5":
version: 2.0.5
resolution: "isarray@npm:2.0.5"
checksum: bd5bbe4104438c4196ba58a54650116007fa0262eccef13a4c55b2e09a5b36b59f1e75b9fcc49883dd9d4953892e6fc007eef9e9155648ceea036e184b0f930a
languageName: node
linkType: hard
"isbinaryfile@npm:^3.0.2": "isbinaryfile@npm:^3.0.2":
version: 3.0.3 version: 3.0.3
resolution: "isbinaryfile@npm:3.0.3" resolution: "isbinaryfile@npm:3.0.3"
@ -11500,6 +11660,7 @@ __metadata:
strip-markdown: ^3.0.3 strip-markdown: ^3.0.3
style-loader: ^0.23.1 style-loader: ^0.23.1
sudo-prompt: ^9.2.1 sudo-prompt: ^9.2.1
tape: ^5.6.0
tempy: ^0.6.0 tempy: ^0.6.0
terser-webpack-plugin: ^4.2.3 terser-webpack-plugin: ^4.2.3
three-full: ^28.0.2 three-full: ^28.0.2
@ -13163,14 +13324,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"object-inspect@npm:^1.12.0, object-inspect@npm:^1.9.0": "object-inspect@npm:^1.12.0, object-inspect@npm:^1.12.2, object-inspect@npm:^1.9.0":
version: 1.12.2 version: 1.12.2
resolution: "object-inspect@npm:1.12.2" resolution: "object-inspect@npm:1.12.2"
checksum: a534fc1b8534284ed71f25ce3a496013b7ea030f3d1b77118f6b7b1713829262be9e6243acbcb3ef8c626e2b64186112cb7f6db74e37b2789b9c789ca23048b2 checksum: a534fc1b8534284ed71f25ce3a496013b7ea030f3d1b77118f6b7b1713829262be9e6243acbcb3ef8c626e2b64186112cb7f6db74e37b2789b9c789ca23048b2
languageName: node languageName: node
linkType: hard linkType: hard
"object-is@npm:^1.0.1": "object-is@npm:^1.0.1, object-is@npm:^1.1.4, object-is@npm:^1.1.5":
version: 1.1.5 version: 1.1.5
resolution: "object-is@npm:1.1.5" resolution: "object-is@npm:1.1.5"
dependencies: dependencies:
@ -13208,6 +13369,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"object.assign@npm:^4.1.3":
version: 4.1.4
resolution: "object.assign@npm:4.1.4"
dependencies:
call-bind: ^1.0.2
define-properties: ^1.1.4
has-symbols: ^1.0.3
object-keys: ^1.1.1
checksum: 76cab513a5999acbfe0ff355f15a6a125e71805fcf53de4e9d4e082e1989bdb81d1e329291e1e4e0ae7719f0e4ef80e88fb2d367ae60500d79d25a6224ac8864
languageName: node
linkType: hard
"object.entries@npm:^1.1.5": "object.entries@npm:^1.1.5":
version: 1.1.5 version: 1.1.5
resolution: "object.entries@npm:1.1.5" resolution: "object.entries@npm:1.1.5"
@ -15513,7 +15686,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"regexp.prototype.flags@npm:^1.2.0, regexp.prototype.flags@npm:^1.4.1, regexp.prototype.flags@npm:^1.4.3": "regexp.prototype.flags@npm:^1.2.0, regexp.prototype.flags@npm:^1.3.0, regexp.prototype.flags@npm:^1.4.1, regexp.prototype.flags@npm:^1.4.3":
version: 1.4.3 version: 1.4.3
resolution: "regexp.prototype.flags@npm:1.4.3" resolution: "regexp.prototype.flags@npm:1.4.3"
dependencies: dependencies:
@ -15959,6 +16132,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"resumer@npm:^0.0.0":
version: 0.0.0
resolution: "resumer@npm:0.0.0"
dependencies:
through: ~2.3.4
checksum: 21b1c257aac24840643fae9bc99ca6447a71a0039e7c6dcf64d0ead447ce511eff158d529f1b6258ad12668e66ee3e49ff14932d2b88a3bd578f483e79708104
languageName: node
linkType: hard
"ret@npm:~0.1.10": "ret@npm:~0.1.10":
version: 0.1.15 version: 0.1.15
resolution: "ret@npm:0.1.15" resolution: "ret@npm:0.1.15"
@ -16546,7 +16728,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"side-channel@npm:^1.0.4": "side-channel@npm:^1.0.3, side-channel@npm:^1.0.4":
version: 1.0.4 version: 1.0.4
resolution: "side-channel@npm:1.0.4" resolution: "side-channel@npm:1.0.4"
dependencies: dependencies:
@ -17139,6 +17321,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"string.prototype.trim@npm:^1.2.6":
version: 1.2.6
resolution: "string.prototype.trim@npm:1.2.6"
dependencies:
call-bind: ^1.0.2
define-properties: ^1.1.4
es-abstract: ^1.19.5
checksum: c5968e023afa9dec6a669c1f427f59aeb74f6f7ee5b0f4b9f0ffcef1d3846aa78b02227448cc874bbfa25dd1f8fd2324041c6cade38d4a986e4ade121ce1ea79
languageName: node
linkType: hard
"string.prototype.trimend@npm:^1.0.5": "string.prototype.trimend@npm:^1.0.5":
version: 1.0.5 version: 1.0.5
resolution: "string.prototype.trimend@npm:1.0.5" resolution: "string.prototype.trimend@npm:1.0.5"
@ -17464,6 +17657,37 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tape@npm:^5.6.0":
version: 5.6.0
resolution: "tape@npm:5.6.0"
dependencies:
array.prototype.every: ^1.1.3
call-bind: ^1.0.2
deep-equal: ^2.0.5
defined: ^1.0.0
dotignore: ^0.1.2
for-each: ^0.3.3
get-package-type: ^0.1.0
glob: ^7.2.3
has: ^1.0.3
has-dynamic-import: ^2.0.1
inherits: ^2.0.4
is-regex: ^1.1.4
minimist: ^1.2.6
object-inspect: ^1.12.2
object-is: ^1.1.5
object-keys: ^1.1.1
object.assign: ^4.1.3
resolve: ^2.0.0-next.3
resumer: ^0.0.0
string.prototype.trim: ^1.2.6
through: ^2.3.8
bin:
tape: bin/tape
checksum: 867b85b6124598c69063548ffb2c4566a63f040d35aee242fd9cc7a0aedc34626feae2ef412d03e2a4817ca9389a4d94006d915b0c163b98e83af52f9258167f
languageName: node
linkType: hard
"tar-stream@npm:^1.5.2": "tar-stream@npm:^1.5.2":
version: 1.6.2 version: 1.6.2
resolution: "tar-stream@npm:1.6.2" resolution: "tar-stream@npm:1.6.2"
@ -17656,7 +17880,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"through@npm:^2.3.6, through@npm:^2.3.8": "through@npm:2, through@npm:^2.3.6, through@npm:^2.3.8, through@npm:~2.3, through@npm:~2.3.1, through@npm:~2.3.4":
version: 2.3.8 version: 2.3.8
resolution: "through@npm:2.3.8" resolution: "through@npm:2.3.8"
checksum: a38c3e059853c494af95d50c072b83f8b676a9ba2818dcc5b108ef252230735c54e0185437618596c790bbba8fcdaef5b290405981ffa09dce67b1f1bf190cbd checksum: a38c3e059853c494af95d50c072b83f8b676a9ba2818dcc5b108ef252230735c54e0185437618596c790bbba8fcdaef5b290405981ffa09dce67b1f1bf190cbd
@ -19106,7 +19330,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"which-boxed-primitive@npm:^1.0.2": "which-boxed-primitive@npm:^1.0.1, which-boxed-primitive@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "which-boxed-primitive@npm:1.0.2" resolution: "which-boxed-primitive@npm:1.0.2"
dependencies: dependencies:
@ -19119,6 +19343,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"which-collection@npm:^1.0.1":
version: 1.0.1
resolution: "which-collection@npm:1.0.1"
dependencies:
is-map: ^2.0.1
is-set: ^2.0.1
is-weakmap: ^2.0.1
is-weakset: ^2.0.1
checksum: c815bbd163107ef9cb84f135e6f34453eaf4cca994e7ba85ddb0d27cea724c623fae2a473ceccfd5549c53cc65a5d82692de418166df3f858e1e5dc60818581c
languageName: node
linkType: hard
"which-module@npm:^2.0.0": "which-module@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "which-module@npm:2.0.0" resolution: "which-module@npm:2.0.0"
@ -19126,6 +19362,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"which-typed-array@npm:^1.1.2":
version: 1.1.8
resolution: "which-typed-array@npm:1.1.8"
dependencies:
available-typed-arrays: ^1.0.5
call-bind: ^1.0.2
es-abstract: ^1.20.0
for-each: ^0.3.3
has-tostringtag: ^1.0.0
is-typed-array: ^1.1.9
checksum: bedf4d30a738e848404fe67fe0ace33433a7298cf3f5a4d4b2c624ba99c4d25f06a7fd6f3566c3d16af5f8a54f0c6293cbfded5b1208ce11812753990223b45a
languageName: node
linkType: hard
"which@npm:^1.2.10, which@npm:^1.2.14, which@npm:^1.2.9, which@npm:^1.3.1": "which@npm:^1.2.10, which@npm:^1.2.14, which@npm:^1.2.9, which@npm:^1.3.1":
version: 1.3.1 version: 1.3.1
resolution: "which@npm:1.3.1" resolution: "which@npm:1.3.1"