feat: profile avatar upload + share_conv_keys rpc + favicon + smtp tweaks
Release desktop app / build (, windows-latest) (push) Has been cancelled
Release desktop app / build (--target universal-apple-darwin --bundles app,updater, macos-14) (push) Has been cancelled

This commit is contained in:
2026-04-19 23:04:03 +02:00
parent ff5ea274b9
commit 0ca29952ba
30 changed files with 3135 additions and 37 deletions
+34 -32
View File
@@ -126,9 +126,17 @@ async function fetchKeyBundle(
};
}
// Strips the leading `\x` postgres bytea hex prefix so the RPC's
// `decode(text, 'hex')` accepts it.
function hexNoPrefix(bytes: Uint8Array): string {
return bytesToPgHex(bytes).slice(2);
}
// Bootstraps a brand-new conv-key, wrapping it for every member device that
// currently exists (including the caller's own devices). Used the first time
// a conversation needs a key, or when rotation is requested.
// a conversation needs a key, or when rotation is requested. All inserts go
// through `share_conv_keys` (SECURITY DEFINER) — silently skips invalid
// recipients, no per-row 403 console spam.
export async function bootstrapConvKey(
client: AppSupabaseClient,
conversationId: string,
@@ -141,29 +149,23 @@ export async function bootstrapConvKey(
throw new Error('cannot bootstrap conv key — no recipient devices');
}
const rows: Array<{
conversation_id: string;
recipient_device_id: string;
key_version: number;
sender_device_id: string;
encrypted_key: string;
nonce: string;
}> = [];
const bundles: Array<{ recipient_device_id: string; encrypted_key: string; nonce: string }> = [];
for (const r of recipients) {
const wrapped = await wrapConvKeyForRecipient(convKey, r.publicKey, own.privateKey);
rows.push({
conversation_id: conversationId,
bundles.push({
recipient_device_id: r.deviceId,
key_version: keyVersion,
sender_device_id: own.deviceId,
encrypted_key: bytesToPgHex(wrapped.ciphertext),
nonce: bytesToPgHex(wrapped.nonce),
encrypted_key: hexNoPrefix(wrapped.ciphertext),
nonce: hexNoPrefix(wrapped.nonce),
});
}
const { error } = await rawFrom(client, 'conversation_keys').upsert(rows, {
onConflict: 'conversation_id,recipient_device_id,key_version',
ignoreDuplicates: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rpc = (client as unknown as { rpc: (n: string, p: object) => Promise<{ error: any }> }).rpc;
const { error } = await rpc.call(client, 'share_conv_keys', {
p_conv_id: conversationId,
p_sender_device_id: own.deviceId,
p_key_version: keyVersion,
p_bundles: bundles,
});
if (error) throw error;
@@ -265,20 +267,20 @@ export async function shareConvKeyToDevice(
recipientPublicKey,
own.privateKey,
);
const { error } = await rawFrom(client, 'conversation_keys').upsert(
{
conversation_id: conversationId,
recipient_device_id: recipientDeviceId,
key_version: version,
sender_device_id: own.deviceId,
encrypted_key: bytesToPgHex(wrapped.ciphertext),
nonce: bytesToPgHex(wrapped.nonce),
},
{
onConflict: 'conversation_id,recipient_device_id,key_version',
ignoreDuplicates: true,
},
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rpc = (client as unknown as { rpc: (n: string, p: object) => Promise<{ error: any }> }).rpc;
const { error } = await rpc.call(client, 'share_conv_keys', {
p_conv_id: conversationId,
p_sender_device_id: own.deviceId,
p_key_version: version,
p_bundles: [
{
recipient_device_id: recipientDeviceId,
encrypted_key: hexNoPrefix(wrapped.ciphertext),
nonce: hexNoPrefix(wrapped.nonce),
},
],
});
if (error) throw error;
}