UNPKG

@river-build/sdk

Version:

For more details, visit the following resources:

256 lines 11.3 kB
import { BaseDecryptionExtensions, } from '@river-build/encryption'; import { make_MemberPayload_KeyFulfillment, make_MemberPayload_KeySolicitation } from './types'; import { Permission } from '@river-build/web3'; import { check } from '@river-build/dlog'; import chunk from 'lodash/chunk'; import { isDefined } from './check'; import { isMobileSafari } from './utils'; import { spaceIdFromChannelId, isDMChannelStreamId, isGDMChannelStreamId, isUserDeviceStreamId, isUserInboxStreamId, isUserSettingsStreamId, isUserStreamId, isChannelStreamId, } from './id'; import { checkEventSignature } from './sign'; export class ClientDecryptionExtensions extends BaseDecryptionExtensions { client; isMobileSafariBackgrounded = false; validatedEvents = {}; unpackEnvelopeOpts; constructor(client, crypto, delegate, userId, userDevice, unpackEnvelopeOpts, logId) { const upToDateStreams = new Set(); client.streams.getStreams().forEach((stream) => { if (stream.isUpToDate) { upToDateStreams.add(stream.streamId); } }); super(client, crypto, delegate, userDevice, userId, upToDateStreams, logId); this.client = client; this.unpackEnvelopeOpts = unpackEnvelopeOpts; const onMembershipChange = (streamId, userId) => { if (userId === this.userId) { this.retryDecryptionFailures(streamId); } }; const onStreamUpToDate = (streamId) => this.setStreamUpToDate(streamId); const onNewGroupSessions = (sessions, senderId) => this.enqueueNewGroupSessions(sessions, senderId); const onNewEncryptedContent = (streamId, eventId, content) => this.enqueueNewEncryptedContent(streamId, eventId, content.kind, content.content); const onKeySolicitation = (streamId, fromUserId, fromUserAddress, keySolicitation, sigBundle) => this.enqueueKeySolicitation(streamId, fromUserId, fromUserAddress, keySolicitation, sigBundle); const onInitKeySolicitations = (streamId, members, sigBundle) => this.enqueueInitKeySolicitations(streamId, members, sigBundle); const onStreamInitialized = (streamId) => { if (isUserInboxStreamId(streamId)) { this.enqueueNewMessageDownload(); } }; const onStreamSyncActive = (active) => { this.log.info('onStreamSyncActive', active); if (!active) { this.resetUpToDateStreams(); } }; client.on('streamUpToDate', onStreamUpToDate); client.on('newGroupSessions', onNewGroupSessions); client.on('newEncryptedContent', onNewEncryptedContent); client.on('newKeySolicitation', onKeySolicitation); client.on('updatedKeySolicitation', onKeySolicitation); client.on('initKeySolicitations', onInitKeySolicitations); client.on('streamNewUserJoined', onMembershipChange); client.on('streamInitialized', onStreamInitialized); client.on('streamSyncActive', onStreamSyncActive); this._onStopFn = () => { client.off('streamUpToDate', onStreamUpToDate); client.off('newGroupSessions', onNewGroupSessions); client.off('newEncryptedContent', onNewEncryptedContent); client.off('newKeySolicitation', onKeySolicitation); client.off('updatedKeySolicitation', onKeySolicitation); client.off('initKeySolicitations', onInitKeySolicitations); client.off('streamNewUserJoined', onMembershipChange); client.off('streamInitialized', onStreamInitialized); client.off('streamSyncActive', onStreamSyncActive); }; this.log.debug('new ClientDecryptionExtensions', { userDevice }); } hasStream(streamId) { const stream = this.client.stream(streamId); return isDefined(stream); } isUserInboxStreamUpToDate(upToDateStreams) { return (this.client.userInboxStreamId !== undefined && upToDateStreams.has(this.client.userInboxStreamId)); } shouldPauseTicking() { return this.isMobileSafariBackgrounded; } async decryptGroupEvent(streamId, eventId, kind, // kind of data encryptedData) { return this.client.decryptGroupEvent(streamId, eventId, kind, encryptedData); } downloadNewMessages() { this.log.info('downloadNewInboxMessages'); return this.client.downloadNewInboxMessages(); } getKeySolicitations(streamId) { const stream = this.client.stream(streamId); return stream?.view.getMembers().joined.get(this.userId)?.solicitations ?? []; } /** * Override the default implementation to use the number of members in the stream * to determine the delay time. */ getRespondDelayMSForKeySolicitation(streamId, userId) { const multiplier = userId === this.userId ? 0.5 : 1; const stream = this.client.stream(streamId); check(isDefined(stream), 'stream not found'); const numMembers = stream.view.getMembers().joinedParticipants().size; const maxWaitTimeSeconds = Math.max(5, Math.min(30, numMembers)); const waitTime = maxWaitTimeSeconds * 1000 * Math.random(); // this could be much better //this.log.debug('getRespondDelayMSForKeySolicitation', { streamId, userId, waitTime }) return waitTime * multiplier; } async isUserEntitledToKeyExchange(streamId, userId, opts) { const stream = this.client.stream(streamId); check(isDefined(stream), 'stream not found'); if (!stream.view.userIsEntitledToKeyExchange(userId)) { this.log.info(`user ${userId} is not a member of stream ${streamId} and cannot request keys`); return false; } if (stream.view.contentKind === 'channelContent' && !(opts?.skipOnChainValidation === true)) { const channel = stream.view.channelContent; const entitlements = await this.entitlementDelegate.isEntitled(channel.spaceId, streamId, userId, Permission.Read); if (!entitlements) { this.log.info('user is not entitled to key exchange'); return false; } } return true; } isValidEvent(item) { if (this.unpackEnvelopeOpts?.disableSignatureValidation !== true) { return { isValid: true }; } const eventId = item.solicitation.srcEventId; const sigBundle = item.sigBundle; if (!sigBundle) { return { isValid: false, reason: 'event not found' }; } if (!sigBundle.signature) { return { isValid: false, reason: 'remote event signature not found' }; } if (this.validatedEvents[eventId]) { return this.validatedEvents[eventId]; } try { checkEventSignature(sigBundle.event, sigBundle.hash, sigBundle.signature); const result = { isValid: true }; this.validatedEvents[eventId] = result; return result; } catch (err) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions const result = { isValid: false, reason: `error: ${err}` }; this.validatedEvents[eventId] = result; return result; } } onDecryptionError(item, err) { this.client.stream(item.streamId)?.updateDecryptedContentError(item.eventId, { missingSession: err.missingSession, kind: err.kind, encryptedData: item.encryptedData, error: err, }); } async ackNewGroupSession(_session) { return this.client.ackInboxStream(); } async encryptAndShareGroupSessions({ streamId, item, sessions, algorithm, }) { const chunked = chunk(sessions, 100); for (const chunk of chunked) { await this.client.encryptAndShareGroupSessions(streamId, chunk, { [item.fromUserId]: [ { deviceKey: item.solicitation.deviceKey, fallbackKey: item.solicitation.fallbackKey, }, ], }, algorithm); } } async sendKeySolicitation({ streamId, isNewDevice, missingSessionIds, }) { const keySolicitation = make_MemberPayload_KeySolicitation({ deviceKey: this.userDevice.deviceKey, fallbackKey: this.userDevice.fallbackKey, isNewDevice, sessionIds: isNewDevice ? [] : missingSessionIds, }); await this.client.makeEventAndAddToStream(streamId, keySolicitation); } async sendKeyFulfillment({ streamId, userAddress, deviceKey, sessionIds, }) { const fulfillment = make_MemberPayload_KeyFulfillment({ userAddress: userAddress, deviceKey: deviceKey, sessionIds: sessionIds, }); const { error } = await this.client.makeEventAndAddToStream(streamId, fulfillment, { optional: true, }); return { error }; } async uploadDeviceKeys() { await this.client.uploadDeviceKeys(); } onStart() { if (isMobileSafari()) { document.addEventListener('visibilitychange', this.mobileSafariPageVisibilityChanged); } } onStop() { if (isMobileSafari()) { document.removeEventListener('visibilitychange', this.mobileSafariPageVisibilityChanged); } return Promise.resolve(); } mobileSafariPageVisibilityChanged = () => { this.log.debug('onMobileSafariBackgrounded', this.isMobileSafariBackgrounded); this.isMobileSafariBackgrounded = document.visibilityState === 'hidden'; if (!this.isMobileSafariBackgrounded) { this.checkStartTicking(); } }; getPriorityForStream(streamId, highPriorityIds, recentStreamIds) { if (isUserDeviceStreamId(streamId) || isUserInboxStreamId(streamId) || isUserStreamId(streamId) || isUserSettingsStreamId(streamId)) { return 0; } // channel or dm we're currently viewing const isChannel = isChannelStreamId(streamId); const isDmOrGdm = isDMChannelStreamId(streamId) || isGDMChannelStreamId(streamId); if ((isDmOrGdm || isChannel) && highPriorityIds.has(streamId)) { return 1; } // if you're getting updates for this stream, decrypt them so that you see unread messages if (recentStreamIds.has(streamId)) { return 2; } // channels in the space we're currently viewing if (isChannel) { const spaceId = spaceIdFromChannelId(streamId); if (highPriorityIds.has(spaceId)) { return 3; } } // dms if (isDmOrGdm) { return 4; } // space that we're currently viewing if (highPriorityIds.has(streamId)) { return 5; } // then other channels, if (isChannel) { return 6; } // then other spaces return 7; } } //# sourceMappingURL=clientDecryptionExtensions.js.map