Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b0f9f1dada | |||
| 961ac2dde5 | |||
| 7efbcf7e39 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "ChatApp",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.3",
|
||||
"identifier": "com.meinname.chatapp",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm vite:dev",
|
||||
|
||||
@@ -164,14 +164,25 @@ async function syncOneConversationGaps(ctx: SyncCtx, convId: string): Promise<vo
|
||||
try {
|
||||
await shareConvKeyToDevice(supabase, convId, dev.id, pgHexToBytes(dev.public_key), ownCtx);
|
||||
} catch (err: unknown) {
|
||||
// Most common: this device hasn't been wrapped for us yet either, so
|
||||
// tryGetConvKey couldn't unwrap. Another peer with the key will fill
|
||||
// the gap when they hit syncAllExistingGaps.
|
||||
console.warn('keySync: shareConvKeyToDevice gap-fill failed', {
|
||||
convId,
|
||||
recipient: dev.id,
|
||||
err,
|
||||
});
|
||||
// Backfill is best-effort. Common silent failures:
|
||||
// - This device hasn't been wrapped for us yet either, so
|
||||
// tryGetConvKey couldn't unwrap (another peer will fill the gap).
|
||||
// - RLS rejects because the recipient's owner is a pending (not-yet
|
||||
// accepted) DM member, or has been removed from the conv.
|
||||
// Any of these are recoverable — log only at debug level.
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const isExpected =
|
||||
msg.includes('does not have it yet') ||
|
||||
msg.includes('row-level security') ||
|
||||
msg.includes('403') ||
|
||||
msg.includes('Forbidden');
|
||||
if (!isExpected) {
|
||||
console.warn('keySync: shareConvKeyToDevice gap-fill failed', {
|
||||
convId,
|
||||
recipient: dev.id,
|
||||
err,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
sendNotification,
|
||||
} from '@tauri-apps/plugin-notification';
|
||||
|
||||
import { isTauriRuntime } from './globalShortcut';
|
||||
|
||||
// Tracks whether permission has already been requested this session so we
|
||||
// don't spam the OS prompt. Actual permission state lives in the OS.
|
||||
let permissionChecked = false;
|
||||
@@ -12,6 +14,11 @@ let permissionGranted = false;
|
||||
export async function ensureNotificationPermission(): Promise<boolean> {
|
||||
if (permissionChecked) return permissionGranted;
|
||||
permissionChecked = true;
|
||||
if (!isTauriRuntime()) {
|
||||
// Web preview / Chrome — Tauri notification plugin not available.
|
||||
permissionGranted = false;
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
let granted = await isPermissionGranted();
|
||||
if (!granted) {
|
||||
@@ -20,7 +27,6 @@ export async function ensureNotificationPermission(): Promise<boolean> {
|
||||
}
|
||||
permissionGranted = granted;
|
||||
} catch (err: unknown) {
|
||||
// Not running under Tauri (e.g. web preview) — fall back silently.
|
||||
permissionGranted = false;
|
||||
console.warn('notification permission check failed', err);
|
||||
}
|
||||
@@ -40,6 +46,7 @@ interface NotifyOpts {
|
||||
|
||||
export async function notify({ title, body, force = false }: NotifyOpts): Promise<void> {
|
||||
if (!force && isAppFocused()) return;
|
||||
if (!isTauriRuntime()) return;
|
||||
const granted = await ensureNotificationPermission();
|
||||
if (!granted) return;
|
||||
try {
|
||||
|
||||
@@ -22,7 +22,17 @@ import sodium from 'libsodium-wrappers';
|
||||
// renamed onto `<file>` so an interrupted write never corrupts the existing
|
||||
// vault.
|
||||
|
||||
const FILE_NAME = 'chatapp-vault.bin';
|
||||
// Per-user vault filename so multiple accounts on the same machine each get
|
||||
// their own file (and Argon2 derives a different key per user, so cross-user
|
||||
// decrypt is also blocked even if filenames collided).
|
||||
async function vaultFileName(userId: string): Promise<string> {
|
||||
const enc = new TextEncoder();
|
||||
const buf = await crypto.subtle.digest('SHA-256', enc.encode('chatapp-vault-name:' + userId));
|
||||
const hex = Array.from(new Uint8Array(buf))
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
return 'chatapp-vault-' + hex.slice(0, 16) + '.bin';
|
||||
}
|
||||
const MAGIC = new TextEncoder().encode('CHATVLT1'); // 8 bytes
|
||||
const SALT_LEN = 16;
|
||||
const NONCE_LEN = 24;
|
||||
@@ -79,7 +89,8 @@ async function deriveKey(userId: string, salt: Uint8Array, s: typeof sodium): Pr
|
||||
async function loadOrCreateVault(userId: string): Promise<VaultState> {
|
||||
const s = await ensureSodium();
|
||||
const dir = await appLocalDataDir();
|
||||
const path = joinPath(dir, FILE_NAME);
|
||||
const fileName = await vaultFileName(userId);
|
||||
const path = joinPath(dir, fileName);
|
||||
const tmpPath = path + '.tmp';
|
||||
|
||||
try {
|
||||
|
||||
@@ -161,7 +161,10 @@ export async function bootstrapConvKey(
|
||||
});
|
||||
}
|
||||
|
||||
const { error } = await rawFrom(client, 'conversation_keys').insert(rows);
|
||||
const { error } = await rawFrom(client, 'conversation_keys').upsert(rows, {
|
||||
onConflict: 'conversation_id,recipient_device_id,key_version',
|
||||
ignoreDuplicates: true,
|
||||
});
|
||||
if (error) throw error;
|
||||
|
||||
const handle = { conversationId, keyVersion, key: convKey };
|
||||
@@ -262,15 +265,21 @@ export async function shareConvKeyToDevice(
|
||||
recipientPublicKey,
|
||||
own.privateKey,
|
||||
);
|
||||
const { error } = await rawFrom(client, 'conversation_keys').insert({
|
||||
conversation_id: conversationId,
|
||||
recipient_device_id: recipientDeviceId,
|
||||
key_version: version,
|
||||
sender_device_id: own.deviceId,
|
||||
encrypted_key: bytesToPgHex(wrapped.ciphertext),
|
||||
nonce: bytesToPgHex(wrapped.nonce),
|
||||
});
|
||||
if (error && !String(error.message ?? '').includes('duplicate')) throw error;
|
||||
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,
|
||||
},
|
||||
);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
// Re-exports for convenience.
|
||||
|
||||
Reference in New Issue
Block a user