feat: Security

This commit is contained in:
2026-06-05 18:34:01 +02:00
parent ee293d7daf
commit 45675192a5
134 changed files with 4128 additions and 446 deletions
@@ -1,5 +1,5 @@
/** IndexedDB schema version - bump when adding/changing object stores. */
export const BROWSER_DATABASE_VERSION = 3;
export const BROWSER_DATABASE_VERSION = 4;
const STORE_MESSAGES = 'messages';
const STORE_USERS = 'users';
@@ -9,6 +9,7 @@ const STORE_BANS = 'bans';
const STORE_META = 'meta';
const STORE_ATTACHMENTS = 'attachments';
const STORE_CUSTOM_EMOJIS = 'customEmojis';
const STORE_MESSAGE_REVISIONS = 'messageRevisions';
export function ensureObjectStoreDuringUpgrade(
database: IDBDatabase,
@@ -64,4 +65,8 @@ export function applyBrowserDatabaseSchema(
ensureStoreIndex(customEmojisStore, 'updatedAt', 'updatedAt');
ensureStoreIndex(customEmojisStore, 'creatorUserId', 'creatorUserId');
const revisionsStore = ensureObjectStoreDuringUpgrade(database, upgradeTransaction, STORE_MESSAGE_REVISIONS, { keyPath: 'id' });
ensureStoreIndex(revisionsStore, 'messageId', 'messageId');
}
@@ -6,7 +6,8 @@ import {
User,
Room,
Reaction,
BanEntry
BanEntry,
type MessageRevision
} from '../../shared-kernel';
import type { ChatAttachmentMeta, CustomEmoji } from '../../shared-kernel';
import { getStoredCurrentUserId } from '../../core/storage/current-user-storage';
@@ -25,6 +26,7 @@ const STORE_BANS = 'bans';
const STORE_META = 'meta';
const STORE_ATTACHMENTS = 'attachments';
const STORE_CUSTOM_EMOJIS = 'customEmojis';
const STORE_MESSAGE_REVISIONS = 'messageRevisions';
/** All object store names, used when clearing the entire database. */
const ALL_STORE_NAMES: string[] = [
STORE_MESSAGES,
@@ -34,6 +36,7 @@ const ALL_STORE_NAMES: string[] = [
STORE_BANS,
STORE_ATTACHMENTS,
STORE_CUSTOM_EMOJIS,
STORE_MESSAGE_REVISIONS,
STORE_META
];
@@ -67,6 +70,13 @@ export class BrowserDatabaseService {
await this.put(STORE_MESSAGES, message);
}
async saveMessageRevision(revision: MessageRevision): Promise<void> {
await this.put(STORE_MESSAGE_REVISIONS, {
...revision,
id: `${revision.messageId}:${revision.revision}`
});
}
/**
* Retrieve the latest messages for a room, sorted oldest-first for display.
* @param roomId - Target room.
@@ -7,7 +7,7 @@ import {
type Room,
type User
} from '../../shared-kernel';
import type { ChatAttachmentMeta, CustomEmoji } from '../../shared-kernel';
import type { ChatAttachmentMeta, CustomEmoji, MessageRevision } from '../../shared-kernel';
import { getStoredCurrentUserId } from '../../core/storage/current-user-storage';
import {
attachmentToValues,
@@ -42,6 +42,15 @@ export class CapacitorDatabaseService {
await this.connection.initialize();
}
async saveMessageRevision(revision: MessageRevision): Promise<void> {
const store = await this.connection.getStore();
await store.run(
'INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)',
[`message-revision:${revision.messageId}:${revision.revision}`, JSON.stringify(revision)]
);
}
async saveMessage(message: Message): Promise<void> {
const store = await this.connection.getStore();
const row = messageToRow(message);
@@ -49,8 +58,9 @@ export class CapacitorDatabaseService {
await store.run(
`INSERT OR REPLACE INTO messages (
id, roomId, ownerUserId, channelId, senderId, senderName, content,
timestamp, editedAt, isDeleted, replyToId, linkMetadata, kind, systemEvent
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
timestamp, editedAt, isDeleted, replyToId, linkMetadata, kind, systemEvent,
revision, headHash
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
row.id,
row.roomId,
@@ -65,7 +75,9 @@ export class CapacitorDatabaseService {
row.replyToId ?? null,
row.linkMetadata ?? null,
row.kind ?? null,
row.systemEvent ?? null
row.systemEvent ?? null,
row.revision ?? 0,
row.headHash ?? null
]
);
}
@@ -9,7 +9,8 @@ import {
User,
Room,
Reaction,
BanEntry
BanEntry,
type MessageRevision
} from '../../shared-kernel';
import type { ChatAttachmentMeta, CustomEmoji } from '../../shared-kernel';
import { PlatformService } from '../../core/platform';
@@ -99,6 +100,11 @@ export class DatabaseService {
/** Persist a single chat message. */
saveMessage(message: Message) { return this.withReady(() => this.backend.saveMessage(message)); }
/** Persist an append-only message revision audit entry. */
saveMessageRevision(revision: MessageRevision) {
return this.withReady(() => this.backend.saveMessageRevision(revision));
}
/** Retrieve the latest messages for a room or channel with optional pagination.
*
* When `beforeTimestamp` is provided, only messages strictly older than that
@@ -4,7 +4,8 @@ import {
User,
Room,
Reaction,
BanEntry
BanEntry,
type MessageRevision
} from '../../shared-kernel';
import type { CustomEmoji } from '../../shared-kernel';
import type { ElectronApi } from '../../core/platform/electron/electron-api.models';
@@ -38,6 +39,16 @@ export class ElectronDatabaseService {
return this.api.command({ type: 'save-message', payload: { message } });
}
saveMessageRevision(revision: MessageRevision): Promise<void> {
return this.api.command({
type: 'save-meta',
payload: {
key: `message-revision:${revision.messageId}:${revision.revision}`,
value: JSON.stringify(revision)
}
});
}
/**
* Retrieve the latest messages for a room, sorted oldest-first for display.
*