@river-build/sdk
Version:
For more details, visit the following resources:
256 lines • 11.3 kB
JavaScript
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