feat signature and delegation verification

This commit is contained in:
Danish Arora 2025-08-18 13:00:27 +05:30
parent 6f7cbb4b45
commit 1dfd790b11
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
11 changed files with 415 additions and 70 deletions

103
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@hookform/resolvers": "^3.9.0",
"@noble/ed25519": "^2.2.3",
"@noble/hashes": "^1.8.0",
"@noble/secp256k1": "^2.3.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.1.0",
@ -64,6 +65,7 @@
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
"vaul": "^0.9.3",
"viem": "^2.34.0",
"wagmi": "^2.16.1",
"zod": "^3.23.8"
},
@ -2129,16 +2131,13 @@
}
},
"node_modules/@noble/secp256k1": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.2.tgz",
"integrity": "sha512-/qzwYl5eFLH8OWIecQWM31qld2g1NfjgylK+TNhqtaUKP37Nm+Y+z30Fjhw0Ct8p9yCQEm2N3W/AckdIb3SMcQ==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"license": "MIT"
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.3.0.tgz",
"integrity": "sha512-0TQed2gcBbIrh7Ccyw+y/uZQvbJwm7Ao4scBUxqpBCcsOlZG0O4KGfjtNAy/li4W8n1xt3dxrwJ0beZ2h2G6Kw==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
@ -5564,6 +5563,18 @@
}
}
},
"node_modules/@waku/enr/node_modules/@noble/secp256k1": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.2.tgz",
"integrity": "sha512-/qzwYl5eFLH8OWIecQWM31qld2g1NfjgylK+TNhqtaUKP37Nm+Y+z30Fjhw0Ct8p9yCQEm2N3W/AckdIb3SMcQ==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"license": "MIT"
},
"node_modules/@waku/interfaces": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.29.tgz",
@ -12069,6 +12080,18 @@
"base64-js": "^1.5.1"
}
},
"node_modules/jsontokens/node_modules/@noble/secp256k1": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.2.tgz",
"integrity": "sha512-/qzwYl5eFLH8OWIecQWM31qld2g1NfjgylK+TNhqtaUKP37Nm+Y+z30Fjhw0Ct8p9yCQEm2N3W/AckdIb3SMcQ==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"license": "MIT"
},
"node_modules/keccak": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz",
@ -15628,9 +15651,9 @@
}
},
"node_modules/viem": {
"version": "2.33.2",
"resolved": "https://registry.npmjs.org/viem/-/viem-2.33.2.tgz",
"integrity": "sha512-/720OaM4dHWs8vXwNpyet+PRERhPaW+n/1UVSCzyb9jkmwwVfaiy/R6YfCFb4v+XXbo8s3Fapa3DM5yCRSkulA==",
"version": "2.34.0",
"resolved": "https://registry.npmjs.org/viem/-/viem-2.34.0.tgz",
"integrity": "sha512-HJZG9Wt0DLX042MG0PK17tpataxtdAEhpta9/Q44FqKwy3xZMI5Lx4jF+zZPuXFuYjZ68R0PXqRwlswHs6r4gA==",
"funding": [
{
"type": "github",
@ -15639,14 +15662,14 @@
],
"license": "MIT",
"dependencies": {
"@noble/curves": "1.9.2",
"@noble/curves": "1.9.6",
"@noble/hashes": "1.8.0",
"@scure/bip32": "1.7.0",
"@scure/bip39": "1.6.0",
"abitype": "1.0.8",
"isows": "1.0.7",
"ox": "0.8.6",
"ws": "8.18.2"
"ox": "0.8.7",
"ws": "8.18.3"
},
"peerDependencies": {
"typescript": ">=5.0.4"
@ -15669,6 +15692,21 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/viem/node_modules/@noble/curves": {
"version": "1.9.6",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.6.tgz",
"integrity": "sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/viem/node_modules/@scure/bip32": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
@ -15703,9 +15741,9 @@
"license": "MIT"
},
"node_modules/viem/node_modules/ox": {
"version": "0.8.6",
"resolved": "https://registry.npmjs.org/ox/-/ox-0.8.6.tgz",
"integrity": "sha512-eiKcgiVVEGDtEpEdFi1EGoVVI48j6icXHce9nFwCNM7CKG3uoCXKdr4TPhS00Iy1TR2aWSF1ltPD0x/YgqIL9w==",
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/ox/-/ox-0.8.7.tgz",
"integrity": "sha512-W1f0FiMf9NZqtHPEDEAEkyzZDwbIKfmH2qmQx8NNiQ/9JhxrSblmtLJsSfTtQG5YKowLOnBlLVguCyxm/7ztxw==",
"funding": [
{
"type": "github",
@ -15732,27 +15770,6 @@
}
}
},
"node_modules/viem/node_modules/ws": {
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/vite": {
"version": "5.4.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz",
@ -16327,9 +16344,9 @@
"license": "ISC"
},
"node_modules/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"

View File

@ -16,6 +16,7 @@
"@hookform/resolvers": "^3.9.0",
"@noble/ed25519": "^2.2.3",
"@noble/hashes": "^1.8.0",
"@noble/secp256k1": "^2.3.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.1.0",
@ -69,6 +70,7 @@
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
"vaul": "^0.9.3",
"viem": "^2.34.0",
"wagmi": "^2.16.1",
"zod": "^3.23.8"
},

View File

@ -21,7 +21,7 @@ interface AuthContextType {
delegationTimeRemaining: () => number;
clearDelegation: () => void;
signMessage: (message: OpchanMessage) => Promise<OpchanMessage | null>;
verifyMessage: (message: OpchanMessage) => boolean;
verifyMessage: (message: OpchanMessage) => Promise<boolean>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
@ -305,7 +305,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
signMessage: async (message: OpchanMessage): Promise<OpchanMessage | null> => {
return authServiceRef.current.signMessage(message);
},
verifyMessage: (message: OpchanMessage): boolean => {
verifyMessage: async (message: OpchanMessage): Promise<boolean> => {
return authServiceRef.current.verifyMessage(message);
}
};

View File

@ -290,7 +290,7 @@ export class AuthService {
/**
* Verify a message signature
*/
verifyMessage(message: OpchanMessage): boolean {
async verifyMessage(message: OpchanMessage): Promise<boolean> {
return this.messageSigning.verifyMessage(message);
}

View File

@ -65,7 +65,7 @@ export class MessageService {
/**
* Verify a message signature
*/
verifyMessage(message: OpchanMessage): boolean {
async verifyMessage(message: OpchanMessage): Promise<boolean> {
return this.authService.verifyMessage(message);
}
}

View File

@ -79,6 +79,7 @@ export class KeyDelegation {
* @param browserPrivateKey The browser-generated private key
* @param duration The duration of the delegation ('1week' or '30days')
* @param walletType The type of wallet (bitcoin or ethereum)
* @param walletPublicKey The public key of the wallet (for signature verification)
* @returns DelegationInfo object
*/
createDelegation(
@ -87,7 +88,8 @@ export class KeyDelegation {
browserPublicKey: string,
browserPrivateKey: string,
duration: DelegationDuration = '7days',
walletType: 'bitcoin' | 'ethereum'
walletType: 'bitcoin' | 'ethereum',
walletPublicKey?: string
): DelegationInfo {
const expiryHours = KeyDelegation.getDurationHours(duration);
const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000);
@ -98,7 +100,8 @@ export class KeyDelegation {
browserPublicKey,
browserPrivateKey,
walletAddress,
walletType
walletType,
walletPublicKey
};
}

View File

@ -0,0 +1,241 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { MessageSigning } from './message-signing';
import { KeyDelegation } from './key-delegation';
import { MessageType, PostMessage } from '@/lib/waku/types';
// Mock the KeyDelegation class
vi.mock('./key-delegation');
// Mock the WalletSignatureVerifier
vi.mock('./wallet-signature-verifier', () => ({
WalletSignatureVerifier: {
verifyWalletSignature: vi.fn().mockReturnValue(true)
}
}));
describe('MessageSigning with Delegation Chain Verification', () => {
let messageSigning: MessageSigning;
let mockKeyDelegation: KeyDelegation;
beforeEach(() => {
// Create a mock delegation
const mockDelegation = {
signature: 'mock-wallet-signature',
expiryTimestamp: Date.now() + 24 * 60 * 60 * 1000, // 24 hours from now
browserPublicKey: 'mock-browser-public-key',
browserPrivateKey: 'mock-browser-private-key',
walletAddress: 'mock-wallet-address',
walletType: 'bitcoin' as const,
walletPublicKey: 'mock-wallet-public-key' // Add wallet public key for verification
};
// Setup mock methods
mockKeyDelegation = {
isDelegationValid: vi.fn().mockReturnValue(true),
retrieveDelegation: vi.fn().mockReturnValue(mockDelegation),
signMessage: vi.fn().mockReturnValue('mock-message-signature'),
verifySignature: vi.fn().mockReturnValue(true),
createDelegationMessage: vi.fn().mockReturnValue('I, mock-wallet-address, delegate authority to this pubkey: mock-browser-public-key until 1234567890'),
getDelegationTimeRemaining: vi.fn().mockReturnValue(24 * 60 * 60 * 1000),
clearDelegation: vi.fn(),
getBrowserPublicKey: vi.fn().mockReturnValue('mock-browser-public-key'),
getDelegatingAddress: vi.fn().mockReturnValue('mock-wallet-address'),
generateKeypair: vi.fn().mockReturnValue({
publicKey: 'mock-browser-public-key',
privateKey: 'mock-browser-private-key'
}),
createDelegation: vi.fn().mockReturnValue(mockDelegation),
storeDelegation: vi.fn()
};
// Mock the KeyDelegation constructor
vi.mocked(KeyDelegation).mockImplementation(() => mockKeyDelegation);
messageSigning = new MessageSigning(mockKeyDelegation);
});
describe('signMessage', () => {
it('should sign a message with delegation chain information', () => {
const message: PostMessage = {
type: MessageType.POST,
timestamp: Date.now(),
author: 'mock-wallet-address',
id: 'test-post-1',
cellId: 'test-cell-1',
title: 'Test Post',
content: 'Test content'
};
const signedMessage = messageSigning.signMessage(message);
expect(signedMessage).not.toBeNull();
expect(signedMessage).toHaveProperty('signature', 'mock-message-signature');
expect(signedMessage).toHaveProperty('browserPubKey', 'mock-browser-public-key');
expect(signedMessage).toHaveProperty('delegationSignature', 'mock-wallet-signature');
expect(signedMessage).toHaveProperty('delegationMessage');
expect(signedMessage).toHaveProperty('delegationExpiry');
});
it('should return null when delegation is invalid', () => {
mockKeyDelegation.isDelegationValid = vi.fn().mockReturnValue(false);
const message: PostMessage = {
type: MessageType.POST,
timestamp: Date.now(),
author: 'mock-wallet-address',
id: 'test-post-1',
cellId: 'test-cell-1',
title: 'Test Post',
content: 'Test content'
};
const signedMessage = messageSigning.signMessage(message);
expect(signedMessage).toBeNull();
});
});
describe('verifyMessage', () => {
it('should verify a valid message with delegation chain', async () => {
const message: PostMessage & {
signature: string;
browserPubKey: string;
delegationSignature: string;
delegationMessage: string;
delegationExpiry: number;
} = {
type: MessageType.POST,
timestamp: Date.now(),
author: 'mock-wallet-address',
id: 'test-post-1',
cellId: 'test-cell-1',
title: 'Test Post',
content: 'Test content',
signature: 'mock-message-signature',
browserPubKey: 'mock-browser-public-key',
delegationSignature: 'mock-wallet-signature',
delegationMessage: 'I, mock-wallet-address, delegate authority to this pubkey: mock-browser-public-key until 1234567890',
delegationExpiry: Date.now() + 24 * 60 * 60 * 1000
};
const isValid = await messageSigning.verifyMessage(message);
expect(isValid).toBe(true);
});
it('should reject message with missing signature fields', async () => {
const message: PostMessage = {
type: MessageType.POST,
timestamp: Date.now(),
author: 'mock-wallet-address',
id: 'test-post-1',
cellId: 'test-cell-1',
title: 'Test Post',
content: 'Test content'
// Missing signature fields
};
const isValid = await messageSigning.verifyMessage(message);
expect(isValid).toBe(false);
});
it('should reject message with missing delegation fields', async () => {
const message: PostMessage & {
signature: string;
browserPubKey: string;
} = {
type: MessageType.POST,
timestamp: Date.now(),
author: 'mock-wallet-address',
id: 'test-post-1',
cellId: 'test-cell-1',
title: 'Test Post',
content: 'Test content',
signature: 'mock-message-signature',
browserPubKey: 'mock-browser-public-key'
// Missing delegation fields
};
const isValid = await messageSigning.verifyMessage(message);
expect(isValid).toBe(false);
});
it('should reject message with invalid signature', async () => {
mockKeyDelegation.verifySignature = vi.fn().mockReturnValue(false);
const message: PostMessage & {
signature: string;
browserPubKey: string;
delegationSignature: string;
delegationMessage: string;
delegationExpiry: number;
} = {
type: MessageType.POST,
timestamp: Date.now(),
author: 'mock-wallet-address',
id: 'test-post-1',
cellId: 'test-cell-1',
title: 'Test Post',
content: 'Test content',
signature: 'invalid-signature',
browserPubKey: 'mock-browser-public-key',
delegationSignature: 'mock-wallet-signature',
delegationMessage: 'I, mock-wallet-address, delegate authority to this pubkey: mock-browser-public-key until 1234567890',
delegationExpiry: Date.now() + 24 * 60 * 60 * 1000
};
const isValid = await messageSigning.verifyMessage(message);
expect(isValid).toBe(false);
});
it('should reject message with expired delegation', async () => {
const message: PostMessage & {
signature: string;
browserPubKey: string;
delegationSignature: string;
delegationMessage: string;
delegationExpiry: number;
} = {
type: MessageType.POST,
timestamp: Date.now(),
author: 'mock-wallet-address',
id: 'test-post-1',
cellId: 'test-cell-1',
title: 'Test Post',
content: 'Test content',
signature: 'mock-message-signature',
browserPubKey: 'mock-browser-public-key',
delegationSignature: 'mock-wallet-signature',
delegationMessage: 'I, mock-wallet-address, delegate authority to this pubkey: mock-browser-public-key until 1234567890',
delegationExpiry: Date.now() - 24 * 60 * 60 * 1000 // Expired 24 hours ago
};
const isValid = await messageSigning.verifyMessage(message);
expect(isValid).toBe(false);
});
it('should reject message with tampered delegation message', async () => {
const message: PostMessage & {
signature: string;
browserPubKey: string;
delegationSignature: string;
delegationMessage: string;
delegationExpiry: number;
} = {
type: MessageType.POST,
timestamp: Date.now(),
author: 'mock-wallet-address',
id: 'test-post-1',
cellId: 'test-cell-1',
title: 'Test Post',
content: 'Test content',
signature: 'mock-message-signature',
browserPubKey: 'mock-browser-public-key',
delegationSignature: 'mock-wallet-signature',
delegationMessage: 'I, attacker-address, delegate authority to this pubkey: mock-browser-public-key until 1234567890', // Tampered
delegationExpiry: Date.now() + 24 * 60 * 60 * 1000
};
const isValid = await messageSigning.verifyMessage(message);
expect(isValid).toBe(false);
});
});
});

View File

@ -1,7 +1,6 @@
import { OpchanMessage } from '@/types';
import { KeyDelegation } from './key-delegation';
import { WalletSignatureVerifier } from './wallet-signature-verifier';
export class MessageSigning {
private keyDelegation: KeyDelegation;
@ -22,7 +21,10 @@ export class MessageSigning {
const messageToSign = JSON.stringify({
...message,
signature: undefined,
browserPubKey: undefined
browserPubKey: undefined,
delegationSignature: undefined,
delegationMessage: undefined,
delegationExpiry: undefined
});
const signature = this.keyDelegation.signMessage(messageToSign);
@ -31,11 +33,24 @@ export class MessageSigning {
return {
...message,
signature,
browserPubKey: delegation.browserPublicKey
browserPubKey: delegation.browserPublicKey,
delegationSignature: delegation.signature,
delegationMessage: this.keyDelegation.createDelegationMessage(
delegation.browserPublicKey,
delegation.walletAddress,
delegation.expiryTimestamp
),
delegationExpiry: delegation.expiryTimestamp
};
}
verifyMessage(message: OpchanMessage): boolean {
async verifyMessage(message: OpchanMessage & {
signature?: string;
browserPubKey?: string;
delegationSignature?: string;
delegationMessage?: string;
delegationExpiry?: number;
}): Promise<boolean> {
// Check for required signature fields
if (!message.signature || !message.browserPubKey) {
const messageId = 'id' in message ? message.id : `${message.type}-${message.timestamp}`;
@ -43,29 +58,95 @@ export class MessageSigning {
return false;
}
// Check for required delegation fields
if (!message.delegationSignature || !message.delegationMessage || !message.delegationExpiry) {
const messageId = 'id' in message ? message.id : `${message.type}-${message.timestamp}`;
console.warn('Message is missing delegation information', messageId);
return false;
}
// Reconstruct the original signed content
// 1. Verify the message signature
const signedContent = JSON.stringify({
...message,
signature: undefined,
browserPubKey: undefined
browserPubKey: undefined,
delegationSignature: undefined,
delegationMessage: undefined,
delegationExpiry: undefined
});
// Verify the signature
const isValid = this.keyDelegation.verifySignature(
const isValidMessageSignature = this.keyDelegation.verifySignature(
signedContent,
message.signature,
message.browserPubKey
);
if (!isValid) {
if (!isValidMessageSignature) {
const messageId = 'id' in message ? message.id : `${message.type}-${message.timestamp}`;
console.warn(`Invalid signature for message ${messageId}`);
console.warn(`Invalid message signature for message ${messageId}`);
return false;
}
return isValid;
// 2. Verify delegation hasn't expired
const now = Date.now();
if (now >= message.delegationExpiry) {
const messageId = 'id' in message ? message.id : `${message.type}-${message.timestamp}`;
console.warn(`Delegation expired for message ${messageId}`);
return false;
}
// 3. Verify delegation message integrity
const expectedDelegationMessage = this.keyDelegation.createDelegationMessage(
message.browserPubKey,
message.author,
message.delegationExpiry
);
if (message.delegationMessage !== expectedDelegationMessage) {
const messageId = 'id' in message ? message.id : `${message.type}-${message.timestamp}`;
console.warn(`Delegation message tampered for message ${messageId}`);
return false;
}
// 4. Verify wallet signature of delegation
const isValidDelegationSignature = await this.verifyWalletSignature(
message.delegationMessage,
message.delegationSignature,
message.author
);
if (!isValidDelegationSignature) {
const messageId = 'id' in message ? message.id : `${message.type}-${message.timestamp}`;
console.warn(`Invalid delegation signature for message ${messageId}`);
return false;
}
return true;
}
/**
* Verify wallet signature of delegation message
* Uses proper cryptographic verification based on wallet type
*/
private async verifyWalletSignature(
delegationMessage: string,
signature: string,
walletAddress: string
): Promise<boolean> {
// Get the wallet type from the delegation
const delegation = this.keyDelegation.retrieveDelegation();
if (!delegation) {
console.warn('No delegation found for wallet signature verification');
return false;
}
// Use the proper wallet signature verifier with public key
return await WalletSignatureVerifier.verifyWalletSignature(
delegationMessage,
signature,
walletAddress,
delegation.walletType,
delegation.walletPublicKey
);
}
}

View File

@ -4,6 +4,7 @@ export interface DelegationSignature {
browserPublicKey: string; // Browser-generated public key that was delegated to
walletAddress: string; // Wallet address that signed the delegation
walletType: 'bitcoin' | 'ethereum'; // Type of wallet that created the delegation
walletPublicKey?: string; // Public key of the wallet (for signature verification)
}
export interface DelegationInfo extends DelegationSignature {

View File

@ -10,14 +10,20 @@ export enum MessageType {
}
/**
* Base interface for all message types
* Base interface for all message types with delegation chain security
*/
export interface BaseMessage {
type: MessageType;
timestamp: number;
author: string;
// Message signature verification fields
signature?: string; // Message signature for verification
browserPubKey?: string; // Public key that signed the message
delegationSignature?: string; // Original wallet signature of delegation
delegationMessage?: string; // Original delegation message that was signed
delegationExpiry?: number; // When the delegation expires
}
/**

View File

@ -74,9 +74,3 @@ export interface Comment {
relevanceScore?: number; // Calculated relevance score
relevanceDetails?: RelevanceScoreDetails; // Detailed breakdown of relevance score calculation
}
// Extended message types for verification
export interface SignedMessage {
signature?: string; // Signature of the message
browserPubKey?: string; // Public key that signed the message
}