@towns-protocol/sdk
Version:
For more details, visit the following resources:
182 lines • 8.87 kB
JavaScript
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