diff --git a/package.json b/package.json index 546cfb21c..0619864a8 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "glob": "7.1.2", "image-type": "^4.1.0", "ip2country": "1.0.1", - "libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.5/libsession_util_nodejs-v0.1.5.tar.gz", + "libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.8/libsession_util_nodejs-v0.1.8.tar.gz", "libsodium-wrappers-sumo": "^0.7.9", "linkify-it": "3.0.2", "lodash": "^4.17.20", diff --git a/ts/session/apis/open_group_api/opengroupV2/OpenGroupManagerV2.ts b/ts/session/apis/open_group_api/opengroupV2/OpenGroupManagerV2.ts index edbb54feb..1bab0b823 100644 --- a/ts/session/apis/open_group_api/opengroupV2/OpenGroupManagerV2.ts +++ b/ts/session/apis/open_group_api/opengroupV2/OpenGroupManagerV2.ts @@ -192,7 +192,11 @@ export class OpenGroupManagerV2 { ); // here, the convo does not exist. Make sure the db & wrappers are clean too await OpenGroupData.removeV2OpenGroupRoom(conversationId); - await SessionUtilUserGroups.removeCommunityFromWrapper(conversationId, fullUrl); + try { + await SessionUtilUserGroups.removeCommunityFromWrapper(conversationId, fullUrl); + } catch (e) { + window.log.warn('failed to removeCommunityFromWrapper', conversationId); + } const room: OpenGroupV2Room = { serverUrl, diff --git a/ts/session/apis/snode_api/SnodeRequestTypes.ts b/ts/session/apis/snode_api/SnodeRequestTypes.ts index b5bfb21cf..69a539a84 100644 --- a/ts/session/apis/snode_api/SnodeRequestTypes.ts +++ b/ts/session/apis/snode_api/SnodeRequestTypes.ts @@ -50,7 +50,8 @@ export type RetrieveSubKeySubRequestType = { export type RetrieveSubRequestType = | RetrieveLegacyClosedGroupSubRequestType | RetrievePubkeySubRequestType - | RetrieveSubKeySubRequestType; + | RetrieveSubKeySubRequestType + | UpdateExpiryOnNodeSubRequest; /** * OXEND_REQUESTS @@ -129,6 +130,21 @@ export type DeleteFromNodeSubRequest = { params: DeleteByHashesFromNodeParams; }; +export type UpdateExpireNodeParams = { + pubkey: string; + pubkey_ed25519: string; + messages: Array; + expiry: number; + signature: string; + extend?: boolean; + shorten?: boolean; +}; + +export type UpdateExpiryOnNodeSubRequest = { + method: 'expire'; + params: UpdateExpireNodeParams; +}; + export type OxendSubRequest = OnsResolveSubRequest | GetServiceNodesSubRequest; export type SnodeApiSubRequests = @@ -138,7 +154,8 @@ export type SnodeApiSubRequests = | StoreOnNodeSubRequest | NetworkTimeSubRequest | DeleteFromNodeSubRequest - | DeleteAllFromNodeSubRequest; + | DeleteAllFromNodeSubRequest + | UpdateExpiryOnNodeSubRequest; // tslint:disable: array-type export type NonEmptyArray = [T, ...T[]]; diff --git a/ts/session/apis/snode_api/expire.ts b/ts/session/apis/snode_api/expire.ts new file mode 100644 index 000000000..7a3d9f8c0 --- /dev/null +++ b/ts/session/apis/snode_api/expire.ts @@ -0,0 +1,237 @@ +import { isEmpty, sample } from 'lodash'; +import { Snode } from '../../../data/data'; +import { getSodiumRenderer } from '../../crypto'; +import { StringUtils, UserUtils } from '../../utils'; +import { fromBase64ToArray, fromHexToArray } from '../../utils/String'; +import { EmptySwarmError } from '../../utils/errors'; +import { UpdateExpireNodeParams } from './SnodeRequestTypes'; +import { doSnodeBatchRequest } from './batchRequest'; +import { GetNetworkTime } from './getNetworkTime'; +import { getSwarmFor } from './snodePool'; +import { SnodeSignature } from './snodeSignatures'; + +async function verifySignature({ + pubkey, + snodePubkey, + expiryApplied, + signature, + messageHashes, + updatedHashes, + unchangedHashes, +}: { + pubkey: string; + snodePubkey: any; + expiryApplied: number; + signature: string; + messageHashes: Array; + updatedHashes: Array; + // only used when shorten or extend is in the request + unchangedHashes?: Record; +}): Promise { + if (!expiryApplied || isEmpty(messageHashes) || isEmpty(signature)) { + window.log.warn('verifySignature missing argument'); + return false; + } + + const edKeyPrivBytes = fromHexToArray(snodePubkey); + /* PUBKEY_HEX || EXPIRY || RMSGs... || UMSGs... || CMSG_EXPs... + where RMSGs are the requested expiry hashes, + UMSGs are the actual updated hashes, and + CMSG_EXPs are (HASH || EXPIRY) values, ascii-sorted by hash, for the unchanged message hashes included in the "unchanged" field. + */ + const hashes = [...messageHashes, ...updatedHashes]; + if (unchangedHashes && Object.keys(unchangedHashes).length > 0) { + hashes.push( + ...Object.entries(unchangedHashes) + .map(([key, value]: [string, string]) => { + return `${key}${value}`; + }) + .sort() + ); + } + + const verificationString = `${pubkey}${expiryApplied}${hashes.join('')}`; + const verificationData = StringUtils.encode(verificationString, 'utf8'); + window.log.debug('verifySignature verificationString', verificationString); + + const sodium = await getSodiumRenderer(); + try { + const isValid = sodium.crypto_sign_verify_detached( + fromBase64ToArray(signature), + new Uint8Array(verificationData), + edKeyPrivBytes + ); + + return isValid; + } catch (e) { + window.log.warn('verifySignature failed with: ', e.message); + return false; + } +} + +async function processExpirationResults( + pubkey: string, + targetNode: Snode, + swarm: Record, + messageHashes: Array +) { + if (isEmpty(swarm)) { + throw Error(`expireOnNodes failed! ${messageHashes}`); + } + + // TODO need proper typing for swarm and results + const results: Record; expiry: number }> = {}; + // window.log.debug(`processExpirationResults start`, swarm, messageHashes); + + for (const nodeKey of Object.keys(swarm)) { + if (!isEmpty(swarm[nodeKey].failed)) { + const reason = 'Unknown'; + const statusCode = '404'; + window?.log?.warn( + `loki_message:::expireMessage - Couldn't delete data from: ${ + targetNode.pubkey_ed25519 + }${reason && statusCode && ` due to an error ${reason} (${statusCode})`}` + ); + // TODO This might be a redundant step + results[nodeKey] = { hashes: [], expiry: 0 }; + } + + const updatedHashes = swarm[nodeKey].updated; + const unchangedHashes = swarm[nodeKey].unchanged; + const expiryApplied = swarm[nodeKey].expiry; + const signature = swarm[nodeKey].signature; + + const isValid = await verifySignature({ + pubkey, + snodePubkey: nodeKey, + expiryApplied, + signature, + messageHashes, + updatedHashes, + unchangedHashes, + }); + + if (!isValid) { + window.log.warn( + 'loki_message:::expireMessage - Signature verification failed!', + messageHashes + ); + } + results[nodeKey] = { hashes: updatedHashes, expiry: expiryApplied }; + } + + return results; +} + +async function expireOnNodes(targetNode: Snode, params: UpdateExpireNodeParams) { + try { + const result = await doSnodeBatchRequest( + [ + { + method: 'expire', + params, + }, + ], + targetNode, + 4000, + params.pubkey, + 'batch' + ); + + if (!result || result.length !== 1 || result[0]?.code !== 200 || !result[0]?.body) { + return false; + } + + try { + // TODOLATER make sure that this code still works once disappearing messages is merged + const parsed = result[0].body; + const expirationResults = await processExpirationResults( + params.pubkey, + targetNode, + parsed.swarm, + params.messages + ); + window.log.debug('expireOnNodes attempt complete. Here are the results', expirationResults); + + return true; + } catch (e) { + window?.log?.warn('expireOnNodes Failed to parse "swarm" result: ', e.msg); + } + return false; + } catch (e) { + window?.log?.warn('expire - send error:', e, `destination ${targetNode.ip}:${targetNode.port}`); + throw e; + } +} + +type ExpireMessageOnSnodeProps = { + messageHash: string; + expireTimer: number; + extend?: boolean; + shorten?: boolean; +}; + +// TODO make this retry in case of updated swarm +export async function expireMessageOnSnode(props: ExpireMessageOnSnodeProps) { + const { messageHash, expireTimer, extend, shorten } = props; + + if (extend && shorten) { + window.log.error( + '[expireMessageOnSnode] We cannot extend and shorten a message at the same time', + messageHash + ); + return; + } + + const shortenOrExtend = shorten ? 'shorten' : extend ? 'extend' : ('' as const); + + const ourPubKey = UserUtils.getOurPubKeyStrFromCache(); + + if (!ourPubKey) { + window.log.eror('[expireMessageOnSnode] No pubkey found', messageHash); + return; + } + + const swarm = await getSwarmFor(ourPubKey); + + const expiry = GetNetworkTime.getNowWithNetworkOffset() + expireTimer; + const signResult = await SnodeSignature.generateUpdateExpirySignature({ + shortenOrExtend, + timestamp: expiry, + messageHashes: [messageHash], + }); + + if (!signResult) { + window.log.error('[expireMessageOnSnode] Signing message expiry on swarm failed', messageHash); + return; + } + + const params: UpdateExpireNodeParams = { + pubkey: ourPubKey, + pubkey_ed25519: signResult.pubkey_ed25519.toUpperCase(), + // TODO better testing for failed case + messages: [messageHash], + expiry, + extend: extend || undefined, + shorten: shorten || undefined, + signature: signResult?.signature, + }; + + const snode = sample(swarm); + if (!snode) { + throw new EmptySwarmError(ourPubKey, 'Ran out of swarm nodes to query'); + } + + try { + // TODO make this whole function `expireMessageOnSnode` retry + const successfulSend = await expireOnNodes(snode, params); + } catch (e) { + const snodeStr = snode ? `${snode.ip}:${snode.port}` : 'null'; + window?.log?.warn( + `loki_message:::expireMessage - ${e.code ? `${e.code} ` : ''}${ + e.message + } by ${ourPubKey} for ${messageHash} via snode:${snodeStr}` + ); + throw e; + } +} diff --git a/ts/session/apis/snode_api/namespaces.ts b/ts/session/apis/snode_api/namespaces.ts index a6d9ef20c..7bfbb7ffa 100644 --- a/ts/session/apis/snode_api/namespaces.ts +++ b/ts/session/apis/snode_api/namespaces.ts @@ -67,7 +67,12 @@ function isUserConfigNamespace(namespace: SnodeNamespaces) { return false; default: - assertUnreachable(namespace, `isUserConfigNamespace case not handled: ${namespace}`); + try { + assertUnreachable(namespace, `isUserConfigNamespace case not handled: ${namespace}`); + } catch (e) { + window.log.warn(`isUserConfigNamespace case not handled: ${namespace}: ${e.message}`); + return false; + } } } diff --git a/ts/session/apis/snode_api/retrieveRequest.ts b/ts/session/apis/snode_api/retrieveRequest.ts index b7909e03c..37f6454b1 100644 --- a/ts/session/apis/snode_api/retrieveRequest.ts +++ b/ts/session/apis/snode_api/retrieveRequest.ts @@ -5,9 +5,12 @@ import { doSnodeBatchRequest } from './batchRequest'; import { GetNetworkTime } from './getNetworkTime'; import { SnodeNamespace, SnodeNamespaces } from './namespaces'; +import { DURATION } from '../../constants'; +import { UserUtils } from '../../utils'; import { RetrieveLegacyClosedGroupSubRequestType, RetrieveSubRequestType, + UpdateExpiryOnNodeSubRequest, } from './SnodeRequestTypes'; import { SnodeSignature } from './snodeSignatures'; import { RetrieveMessagesResultsBatched, RetrieveMessagesResultsContent } from './types'; @@ -16,9 +19,10 @@ async function buildRetrieveRequest( lastHashes: Array, pubkey: string, namespaces: Array, - ourPubkey: string + ourPubkey: string, + configHashesToBump: Array | null ): Promise> { - const retrieveRequestsParams = await Promise.all( + const retrieveRequestsParams: Array = await Promise.all( namespaces.map(async (namespace, index) => { const retrieveParam = { pubkey, @@ -65,6 +69,33 @@ async function buildRetrieveRequest( return retrieve; }) ); + + if (configHashesToBump?.length) { + const expiry = GetNetworkTime.getNowWithNetworkOffset() + DURATION.DAYS * 30; + const signResult = await SnodeSignature.generateUpdateExpirySignature({ + shortenOrExtend: '', + timestamp: expiry, + messageHashes: configHashesToBump, + }); + if (!signResult) { + window.log.warn( + `SnodeSignature.generateUpdateExpirySignature returned result empty for hashes ${configHashesToBump}` + ); + } else { + const expireParams: UpdateExpiryOnNodeSubRequest = { + method: 'expire', + params: { + messages: configHashesToBump, + pubkey: UserUtils.getOurPubKeyStrFromCache(), + expiry, + signature: signResult.signature, + pubkey_ed25519: signResult.pubkey_ed25519, + }, + }; + + retrieveRequestsParams.push(expireParams); + } + } return retrieveRequestsParams; } @@ -73,7 +104,8 @@ async function retrieveNextMessages( lastHashes: Array, associatedWith: string, namespaces: Array, - ourPubkey: string + ourPubkey: string, + configHashesToBump: Array | null ): Promise { if (namespaces.length !== lastHashes.length) { throw new Error('namespaces and lasthashes does not match'); @@ -83,7 +115,8 @@ async function retrieveNextMessages( lastHashes, associatedWith, namespaces, - ourPubkey + ourPubkey, + configHashesToBump ); // let exceptions bubble up // no retry for this one as this a call we do every few seconds while polling for messages @@ -99,7 +132,8 @@ async function retrieveNextMessages( ); } - if (results.length !== namespaces.length) { + // the +1 is to take care of the extra `expire` method added once user config is released + if (results.length !== namespaces.length && results.length !== namespaces.length + 1) { throw new Error( `We asked for updates about ${namespaces.length} messages but got results of length ${results.length}` ); diff --git a/ts/session/apis/snode_api/snodeSignatures.ts b/ts/session/apis/snode_api/snodeSignatures.ts index 2a6b20a89..9402107de 100644 --- a/ts/session/apis/snode_api/snodeSignatures.ts +++ b/ts/session/apis/snode_api/snodeSignatures.ts @@ -94,4 +94,47 @@ async function getSnodeSignatureParams(params: { } } -export const SnodeSignature = { getSnodeSignatureParams, getSnodeSignatureByHashesParams }; +async function generateUpdateExpirySignature({ + shortenOrExtend, + timestamp, + messageHashes, +}: { + shortenOrExtend: 'extend' | 'shorten' | ''; + timestamp: number; + messageHashes: Array; +}): Promise<{ signature: string; pubkey_ed25519: string } | null> { + const ourEd25519Key = await UserUtils.getUserED25519KeyPair(); + + if (!ourEd25519Key) { + const err = `getSnodeSignatureParams "expiry": User has no getUserED25519KeyPair()`; + window.log.warn(err); + throw new Error(err); + } + + const edKeyPrivBytes = fromHexToArray(ourEd25519Key?.privKey); + + // "expire" || ShortenOrExtend || expiry || messages[0] || ... || messages[N] + const verificationString = `expire${shortenOrExtend}${timestamp}${messageHashes.join('')}`; + const verificationData = StringUtils.encode(verificationString, 'utf8'); + const message = new Uint8Array(verificationData); + + const sodium = await getSodiumRenderer(); + try { + const signature = sodium.crypto_sign_detached(message, edKeyPrivBytes); + const signatureBase64 = fromUInt8ArrayToBase64(signature); + + return { + signature: signatureBase64, + pubkey_ed25519: ourEd25519Key.pubKey, + }; + } catch (e) { + window.log.warn('generateSignature failed with: ', e.message); + return null; + } +} + +export const SnodeSignature = { + getSnodeSignatureParams, + getSnodeSignatureByHashesParams, + generateUpdateExpirySignature, +}; diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index afc9865e4..90e54c442 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -22,6 +22,8 @@ import { SnodeNamespace, SnodeNamespaces } from './namespaces'; import { SnodeAPIRetrieve } from './retrieveRequest'; import { RetrieveMessageItem, RetrieveMessagesResultsBatched } from './types'; import { ReleasedFeatures } from '../../../util/releaseFeature'; +import { LibSessionUtil } from '../../utils/libsession/libsession_utils'; +import { GenericWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface'; export function extractWebSocketContent( message: string, @@ -215,7 +217,12 @@ export class SwarmPolling { let resultsFromAllNamespaces: RetrieveMessagesResultsBatched | null; try { - resultsFromAllNamespaces = await this.pollNodeForKey(toPollFrom, pubkey, namespaces); + resultsFromAllNamespaces = await this.pollNodeForKey( + toPollFrom, + pubkey, + namespaces, + !isGroup + ); } catch (e) { window.log.warn( `pollNodeForKey of ${pubkey} namespaces: ${namespaces} failed with: ${e.message}` @@ -371,39 +378,69 @@ export class SwarmPolling { private async pollNodeForKey( node: Snode, pubkey: PubKey, - namespaces: Array + namespaces: Array, + isUs: boolean ): Promise { const namespaceLength = namespaces.length; if (namespaceLength <= 0) { throw new Error(`invalid number of retrieve namespace provided: ${namespaceLength}`); } - const edkey = node.pubkey_ed25519; + const snodeEdkey = node.pubkey_ed25519; const pkStr = pubkey.key; try { return await pRetry( async () => { const prevHashes = await Promise.all( - namespaces.map(namespace => this.getLastHash(edkey, pkStr, namespace)) + namespaces.map(namespace => this.getLastHash(snodeEdkey, pkStr, namespace)) ); - const results = await SnodeAPIRetrieve.retrieveNextMessages( + const configHashesToBump: Array = []; + + if (await ReleasedFeatures.checkIsUserConfigFeatureReleased()) { + // TODOLATER add the logic to take care of the closed groups too once we have a way to do it with the wrappers + if (isUs) { + for (let index = 0; index < LibSessionUtil.requiredUserVariants.length; index++) { + const variant = LibSessionUtil.requiredUserVariants[index]; + try { + const toBump = await GenericWrapperActions.currentHashes(variant); + + if (toBump?.length) { + configHashesToBump.push(...toBump); + } + } catch (e) { + window.log.warn(`failed to get currentHashes for user variant ${variant}`); + } + } + window.log.debug(`configHashesToBump: ${configHashesToBump}`); + } + } + + let results = await SnodeAPIRetrieve.retrieveNextMessages( node, prevHashes, pkStr, namespaces, - UserUtils.getOurPubKeyStrFromCache() + UserUtils.getOurPubKeyStrFromCache(), + configHashesToBump ); + if (!results.length) { return []; } - - if (results.length !== namespaceLength) { - window.log.error( - `pollNodeForKey asked for ${namespaceLength} namespaces but received only messages about ${results.length} namespaces` - ); - throw new Error( - `pollNodeForKey asked for ${namespaceLength} namespaces but received only messages about ${results.length} namespaces` - ); + // when we asked to extend the expiry of the config messages, exclude it from the list of results as we do not want to mess up the last hash tracking logic + if (configHashesToBump.length) { + try { + const lastResult = results[results.length - 1]; + if (lastResult?.code !== 200) { + // the update expiry of our config messages didn't work. + window.log.warn( + `the update expiry of our tracked config hashes didn't work: ${JSON.stringify( + lastResult + )}` + ); + } + } catch (e) {} + results = results.slice(0, results.length - 1); } const lastMessages = results.map(r => { @@ -416,7 +453,7 @@ export class SwarmPolling { return; } return this.updateLastHash({ - edkey: edkey, + edkey: snodeEdkey, pubkey, namespace: namespaces[index], hash: lastMessage.hash, diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 8101c69c6..a49108ffe 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -238,13 +238,17 @@ export class ConversationController { try { const fromWrapper = await UserGroupsWrapperActions.getCommunityByFullUrl(conversation.id); - - await SessionUtilConvoInfoVolatile.removeCommunityFromWrapper( - conversation.id, - fromWrapper?.fullUrl || '' - ); + if (fromWrapper?.fullUrl) { + await SessionUtilConvoInfoVolatile.removeCommunityFromWrapper( + conversation.id, + fromWrapper.fullUrl + ); + } } catch (e) { - window?.log?.info('SessionUtilConvoInfoVolatile.removeCommunityFromWrapper failed:', e); + window?.log?.info( + 'SessionUtilConvoInfoVolatile.removeCommunityFromWrapper failed:', + e.message + ); } // remove from the wrapper the entries before we remove the roomInfos, as we won't have the required community pubkey afterwards diff --git a/ts/util/releaseFeature.ts b/ts/util/releaseFeature.ts index 2465bb636..e8fc4965f 100644 --- a/ts/util/releaseFeature.ts +++ b/ts/util/releaseFeature.ts @@ -84,9 +84,9 @@ async function checkIsFeatureReleased(featureName: FeatureNameTracked): Promise< } const isReleased = Boolean(getIsFeatureReleasedCached(featureName)); - window.log.debug( - `[releaseFeature]: "${featureName}" ${isReleased ? 'is released' : 'has not been released yet'}` - ); + // window.log.debug( + // `[releaseFeature]: "${featureName}" ${isReleased ? 'is released' : 'has not been released yet'}` + // ); return isReleased; } diff --git a/ts/webworker/workers/browser/libsession_worker_interface.ts b/ts/webworker/workers/browser/libsession_worker_interface.ts index 807042443..115a8f0ac 100644 --- a/ts/webworker/workers/browser/libsession_worker_interface.ts +++ b/ts/webworker/workers/browser/libsession_worker_interface.ts @@ -75,6 +75,10 @@ export const GenericWrapperActions = { callLibSessionWorker([wrapperId, 'storageNamespace']) as Promise< ReturnType >, + currentHashes: async (wrapperId: ConfigWrapperObjectTypes) => + callLibSessionWorker([wrapperId, 'currentHashes']) as Promise< + ReturnType + >, }; export const UserConfigWrapperActions: UserConfigWrapperActionsCalls = { @@ -90,6 +94,7 @@ export const UserConfigWrapperActions: UserConfigWrapperActionsCalls = { needsPush: async () => GenericWrapperActions.needsPush('UserConfig'), push: async () => GenericWrapperActions.push('UserConfig'), storageNamespace: async () => GenericWrapperActions.storageNamespace('UserConfig'), + currentHashes: async () => GenericWrapperActions.currentHashes('UserConfig'), /** UserConfig wrapper specific actions */ getUserInfo: async () => @@ -119,6 +124,7 @@ export const ContactsWrapperActions: ContactsWrapperActionsCalls = { needsPush: async () => GenericWrapperActions.needsPush('ContactsConfig'), push: async () => GenericWrapperActions.push('ContactsConfig'), storageNamespace: async () => GenericWrapperActions.storageNamespace('ContactsConfig'), + currentHashes: async () => GenericWrapperActions.currentHashes('ContactsConfig'), /** ContactsConfig wrapper specific actions */ get: async (pubkeyHex: string) => @@ -154,6 +160,7 @@ export const UserGroupsWrapperActions: UserGroupsWrapperActionsCalls = { needsPush: async () => GenericWrapperActions.needsPush('UserGroupsConfig'), push: async () => GenericWrapperActions.push('UserGroupsConfig'), storageNamespace: async () => GenericWrapperActions.storageNamespace('UserGroupsConfig'), + currentHashes: async () => GenericWrapperActions.currentHashes('UserGroupsConfig'), /** UserGroups wrapper specific actions */ @@ -226,6 +233,7 @@ export const ConvoInfoVolatileWrapperActions: ConvoInfoVolatileWrapperActionsCal needsPush: async () => GenericWrapperActions.needsPush('ConvoInfoVolatileConfig'), push: async () => GenericWrapperActions.push('ConvoInfoVolatileConfig'), storageNamespace: async () => GenericWrapperActions.storageNamespace('ConvoInfoVolatileConfig'), + currentHashes: async () => GenericWrapperActions.currentHashes('ConvoInfoVolatileConfig'), /** ConvoInfoVolatile wrapper specific actions */ // 1o1 diff --git a/yarn.lock b/yarn.lock index 90703f5d5..5292a63e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5148,9 +5148,9 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -"libsession_util_nodejs@https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.5/libsession_util_nodejs-v0.1.5.tar.gz": - version "0.1.5" - resolved "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.5/libsession_util_nodejs-v0.1.5.tar.gz#4f9dbac0c3011fc3edd29ce0625cba1044db6990" +"libsession_util_nodejs@https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.8/libsession_util_nodejs-v0.1.8.tar.gz": + version "0.1.8" + resolved "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.8/libsession_util_nodejs-v0.1.8.tar.gz#49a296dc1d81db5ec8104ef1e6ce5ed2cecff6e9" dependencies: cmake-js "^7.2.1" node-addon-api "^6.1.0"