UNPKG

@towns-protocol/sdk

Version:

For more details, visit the following resources:

182 lines 8.87 kB
import { GroupEncryptionAlgorithmId, GroupEncryptionCrypto, } from '@towns-protocol/encryption'; import { makeStreamRpcClient } from './makeStreamRpcClient'; import { makeSignerContext, makeSignerContextFromBearerToken, } from './signerContext'; import { makeRiverConfig } from './riverConfig'; import { ethers } from 'ethers'; import { RiverRegistry } from '@towns-protocol/web3'; import { makeSessionKeys } from './decryptionExtensions'; import { makeRiverProvider } from './sync-agent/utils/providers'; import { RiverDbManager } from './riverDbManager'; import { makeUserInboxStreamId, makeUserMetadataStreamId, streamIdAsBytes, userIdFromAddress, } from './id'; import { make_UserInboxPayload_GroupEncryptionSessions, } from './types'; import { makeEvent, unpackStream, unpackEnvelope as sdk_unpackEnvelope, unpackEnvelopes as sdk_unpackEnvelopes, } from './sign'; import { bin_toHexString, check } from '@towns-protocol/dlog'; import { toJsonString } from '@bufbuild/protobuf'; import { SessionKeysSchema } from '@towns-protocol/proto'; export const createTownsClient = async (params) => { const config = makeRiverConfig(params.env); let signer; if ('mnemonic' in params) { const wallet = ethers.Wallet.fromMnemonic(params.mnemonic); const delegateWallet = ethers.Wallet.createRandom(); signer = await makeSignerContext(wallet, delegateWallet); } else if ('privateKey' in params) { const wallet = new ethers.Wallet(params.privateKey); const delegateWallet = ethers.Wallet.createRandom(); signer = await makeSignerContext(wallet, delegateWallet); } else { signer = await makeSignerContextFromBearerToken(params.bearerToken); } const riverProvider = makeRiverProvider(config); const riverRegistryDapp = new RiverRegistry(config.river.chainConfig, riverProvider); const urls = await riverRegistryDapp.getOperationalNodeUrls(); const rpc = makeStreamRpcClient(urls, () => riverRegistryDapp.getOperationalNodeUrls()); const userId = userIdFromAddress(signer.creatorAddress); const cryptoStore = RiverDbManager.getCryptoDb(userId); await cryptoStore.initialize(); // eslint-disable-next-line prefer-const let crypto; const getStream = async (streamId) => { const { disableHashValidation, disableSignatureValidation } = client; const stream = await client.rpc.getStream({ streamId: streamIdAsBytes(streamId) }); return unpackStream(stream.stream, { disableHashValidation, disableSignatureValidation, }); }; const unpackEnvelope = async (envelope) => { const { disableHashValidation, disableSignatureValidation } = client; return sdk_unpackEnvelope(envelope, { disableHashValidation, disableSignatureValidation, }); }; const unpackEnvelopes = async (envelopes) => { const { disableHashValidation, disableSignatureValidation } = client; return sdk_unpackEnvelopes(envelopes, { disableHashValidation, disableSignatureValidation }); }; const buildGroupEncryptionClient = () => { const getMiniblockInfo = async (streamId) => { const { streamAndCookie } = await getStream(streamId); return { miniblockNum: streamAndCookie.miniblocks[0].header.miniblockNum, miniblockHash: streamAndCookie.miniblocks[0].hash, }; }; const downloadUserDeviceInfo = async (userIds) => { const forceDownload = userIds.length <= 10; const promises = userIds.map(async (userId) => { const streamId = makeUserMetadataStreamId(userId); try { // also always download your own keys so you always share to your most up to date devices if (!forceDownload && userId !== userId) { const devicesFromStore = await cryptoStore.getUserDevices(userId); if (devicesFromStore.length > 0) { return { userId, devices: devicesFromStore }; } } // return latest 10 device keys const deviceLookback = 10; const stream = await getStream(streamId); const encryptionDevices = stream.snapshot.content.case === 'userMetadataContent' ? stream.snapshot.content.value.encryptionDevices : []; const userDevices = encryptionDevices.slice(-deviceLookback); await cryptoStore.saveUserDevices(userId, userDevices); return { userId, devices: userDevices }; } catch (e) { return { userId, devices: [] }; } }); return (await Promise.all(promises)).reduce((acc, current) => { acc[current.userId] = current.devices; return acc; }, {}); }; const encryptAndShareGroupSessions = async (streamId, sessions, toDevices, algorithm) => { check(sessions.length >= 0, 'no sessions to encrypt'); check(new Set(sessions.map((s) => s.streamId)).size === 1, 'sessions should all be from the same stream'); check(sessions[0].algorithm === algorithm, 'algorithm mismatch'); check(new Set(sessions.map((s) => s.algorithm)).size === 1, 'all sessions should be the same algorithm'); check(sessions[0].streamId === streamId, 'streamId mismatch'); const userDevice = crypto.getUserDevice(); const sessionIds = sessions.map((session) => session.sessionId); const payload = makeSessionKeys(sessions); const promises = Object.entries(toDevices).map(async ([userId, deviceKeys]) => { try { const ciphertext = await crypto.encryptWithDeviceKeys(toJsonString(SessionKeysSchema, payload), deviceKeys); if (Object.keys(ciphertext).length === 0) { return; } const toStreamId = makeUserInboxStreamId(userId); const { hash: miniblockHash } = await rpc.getLastMiniblockHash({ streamId: streamIdAsBytes(toStreamId), }); const event = await makeEvent(signer, make_UserInboxPayload_GroupEncryptionSessions({ streamId: streamIdAsBytes(toStreamId), senderKey: userDevice.deviceKey, sessionIds: sessionIds, ciphertexts: ciphertext, algorithm: algorithm, }), miniblockHash); const eventId = bin_toHexString(event.hash); const { error } = await rpc.addEvent({ streamId: streamIdAsBytes(streamId), event, optional: false, }); return { miniblockHash, eventId, error }; } catch { return undefined; } }); await Promise.all(promises); }; const getDevicesInStream = async (streamId) => { const stream = await getStream(streamId); if (!stream) { return {}; } const members = stream.snapshot.members?.joined.map((x) => userIdFromAddress(x.userAddress)); return downloadUserDeviceInfo(members ?? [], true); }; return { getMiniblockInfo, downloadUserDeviceInfo, encryptAndShareGroupSessions, getDevicesInStream, }; }; await cryptoStore.initialize(); crypto = new GroupEncryptionCrypto(buildGroupEncryptionClient(), cryptoStore); await crypto.init(params.encryptionDevice); const client = { crypto, keychain: cryptoStore, defaultGroupEncryptionAlgorithm: GroupEncryptionAlgorithmId.HybridGroupEncryption, rpc, signer, userId, disableHashValidation: false, disableSignatureValidation: false, getStream, unpackEnvelope, unpackEnvelopes, env: config.environmentId, }; function extend(base) { return (extendFn) => { const extended = extendFn(base); for (const key in client) delete extended[key]; const combined = { ...base, ...extended }; return Object.assign(combined, { extend: extend(combined) }); }; } return Object.assign(client, { extend: extend(client) }); }; //# sourceMappingURL=client-v2.js.map