@quilibrium/quorum-shared 2.1.0-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/index.d.mts +2414 -0
  2. package/dist/index.d.ts +2414 -0
  3. package/dist/index.js +2788 -0
  4. package/dist/index.mjs +2678 -0
  5. package/package.json +49 -0
  6. package/src/api/client.ts +86 -0
  7. package/src/api/endpoints.ts +87 -0
  8. package/src/api/errors.ts +179 -0
  9. package/src/api/index.ts +35 -0
  10. package/src/crypto/encryption-state.ts +249 -0
  11. package/src/crypto/index.ts +55 -0
  12. package/src/crypto/types.ts +307 -0
  13. package/src/crypto/wasm-provider.ts +298 -0
  14. package/src/hooks/index.ts +31 -0
  15. package/src/hooks/keys.ts +62 -0
  16. package/src/hooks/mutations/index.ts +15 -0
  17. package/src/hooks/mutations/useDeleteMessage.ts +67 -0
  18. package/src/hooks/mutations/useEditMessage.ts +87 -0
  19. package/src/hooks/mutations/useReaction.ts +163 -0
  20. package/src/hooks/mutations/useSendMessage.ts +131 -0
  21. package/src/hooks/useChannels.ts +49 -0
  22. package/src/hooks/useMessages.ts +77 -0
  23. package/src/hooks/useSpaces.ts +60 -0
  24. package/src/index.ts +32 -0
  25. package/src/signing/index.ts +10 -0
  26. package/src/signing/types.ts +83 -0
  27. package/src/signing/wasm-provider.ts +75 -0
  28. package/src/storage/adapter.ts +118 -0
  29. package/src/storage/index.ts +9 -0
  30. package/src/sync/index.ts +83 -0
  31. package/src/sync/service.test.ts +822 -0
  32. package/src/sync/service.ts +947 -0
  33. package/src/sync/types.ts +267 -0
  34. package/src/sync/utils.ts +588 -0
  35. package/src/transport/browser-websocket.ts +299 -0
  36. package/src/transport/index.ts +34 -0
  37. package/src/transport/rn-websocket.ts +321 -0
  38. package/src/transport/types.ts +56 -0
  39. package/src/transport/websocket.ts +212 -0
  40. package/src/types/bookmark.ts +29 -0
  41. package/src/types/conversation.ts +25 -0
  42. package/src/types/index.ts +57 -0
  43. package/src/types/message.ts +178 -0
  44. package/src/types/space.ts +75 -0
  45. package/src/types/user.ts +72 -0
  46. package/src/utils/encoding.ts +106 -0
  47. package/src/utils/formatting.ts +139 -0
  48. package/src/utils/index.ts +9 -0
  49. package/src/utils/logger.ts +141 -0
  50. package/src/utils/mentions.ts +135 -0
  51. package/src/utils/validation.ts +84 -0
@@ -0,0 +1,212 @@
1
+ /**
2
+ * WebSocket client interface
3
+ *
4
+ * Platform-agnostic WebSocket connection management.
5
+ * Handles connection, reconnection, and message routing.
6
+ *
7
+ * IMPORTANT: This handles transport only. Encryption is handled separately
8
+ * by the CryptoProvider before/after message transmission.
9
+ */
10
+
11
+ // ============ Connection State ============
12
+
13
+ export type WebSocketConnectionState = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
14
+
15
+ // ============ Message Types ============
16
+
17
+ /**
18
+ * Encrypted message received from WebSocket
19
+ * The content is a SealedMessage containing the encrypted envelope.
20
+ * All cryptographic material (ephemeral key, identity key) comes from the envelope itself.
21
+ */
22
+ export interface EncryptedWebSocketMessage {
23
+ /** The encrypted message content (JSON string of SealedMessage) */
24
+ encryptedContent: string;
25
+ /** The inbox address this message was sent to */
26
+ inboxAddress: string;
27
+ /** Server timestamp */
28
+ timestamp: number;
29
+ }
30
+
31
+ /**
32
+ * SealedMessage format from the network
33
+ * Contains an encrypted envelope that can be unsealed with device keys.
34
+ * The ephemeral_public_key is used along with the inbox private key to decrypt.
35
+ */
36
+ export interface SealedMessage {
37
+ /** The inbox address the message was sent to */
38
+ inbox_address: string;
39
+ /** Sender's ephemeral public key (used for decryption) */
40
+ ephemeral_public_key: string;
41
+ /** The encrypted envelope (contains identity key, message, return inbox info) */
42
+ envelope: string;
43
+ /** Optional inbox public key for verification */
44
+ inbox_public_key?: string;
45
+ /** Optional inbox signature for verification */
46
+ inbox_signature?: string;
47
+ }
48
+
49
+ /**
50
+ * UnsealedEnvelope - the decrypted content of a SealedMessage
51
+ * Contains all the data needed to establish a session and decrypt the message.
52
+ */
53
+ export interface UnsealedEnvelope {
54
+ /** Sender's user address */
55
+ user_address: string;
56
+ /** Sender's display name */
57
+ display_name?: string;
58
+ /** Sender's icon URL */
59
+ user_icon?: string;
60
+ /** Inbox address for sending replies */
61
+ return_inbox_address: string;
62
+ /** Encryption key for the return inbox */
63
+ return_inbox_encryption_key: string;
64
+ /** Public key for the return inbox */
65
+ return_inbox_public_key: string;
66
+ /** Private key for the return inbox (only for sender's own use) */
67
+ return_inbox_private_key: string;
68
+ /** Sender's identity public key (for X3DH) */
69
+ identity_public_key: string;
70
+ /** Session/conversation tag */
71
+ tag: string;
72
+ /** The encrypted/signed message content */
73
+ message: string;
74
+ /** Message type */
75
+ type: string;
76
+ /** The ephemeral public key used to seal this message */
77
+ ephemeral_public_key: string;
78
+ }
79
+
80
+ /**
81
+ * Outbound message to send via WebSocket
82
+ */
83
+ export interface OutboundWebSocketMessage {
84
+ /** Message type (e.g., 'message', 'listen', 'unlisten') */
85
+ type: string;
86
+ /** Message payload */
87
+ payload: unknown;
88
+ }
89
+
90
+ /**
91
+ * Listen/subscribe message for inbox addresses
92
+ */
93
+ export interface ListenMessage {
94
+ type: 'listen';
95
+ inbox_addresses: string[];
96
+ }
97
+
98
+ /**
99
+ * Unlisten/unsubscribe message
100
+ */
101
+ export interface UnlistenMessage {
102
+ type: 'unlisten';
103
+ inbox_addresses: string[];
104
+ }
105
+
106
+ // ============ Configuration ============
107
+
108
+ export interface WebSocketClientOptions {
109
+ /** WebSocket server URL */
110
+ url: string;
111
+ /** Interval between reconnection attempts (ms). Default: 1000 */
112
+ reconnectInterval?: number;
113
+ /** Maximum reconnection attempts. Default: Infinity */
114
+ maxReconnectAttempts?: number;
115
+ /** Queue processing interval (ms). Default: 1000 */
116
+ queueProcessInterval?: number;
117
+ }
118
+
119
+ // ============ Event Handlers ============
120
+
121
+ export type MessageHandler = (message: EncryptedWebSocketMessage) => Promise<void>;
122
+ export type StateChangeHandler = (state: WebSocketConnectionState) => void;
123
+ export type ErrorHandler = (error: Error) => void;
124
+
125
+ // ============ WebSocket Client Interface ============
126
+
127
+ /**
128
+ * WebSocketClient - Platform-agnostic WebSocket interface
129
+ *
130
+ * Features:
131
+ * - Automatic reconnection with configurable interval
132
+ * - Dual queue system (inbound/outbound)
133
+ * - Inbox address subscription management
134
+ * - Message handler registration
135
+ *
136
+ * Implementations:
137
+ * - Browser/Electron: Uses native WebSocket API
138
+ * - React Native: Uses React Native WebSocket
139
+ */
140
+ export interface WebSocketClient {
141
+ /**
142
+ * Current connection state
143
+ */
144
+ readonly state: WebSocketConnectionState;
145
+
146
+ /**
147
+ * Whether currently connected
148
+ */
149
+ readonly isConnected: boolean;
150
+
151
+ /**
152
+ * Connect to the WebSocket server
153
+ */
154
+ connect(): Promise<void>;
155
+
156
+ /**
157
+ * Disconnect from the WebSocket server
158
+ */
159
+ disconnect(): void;
160
+
161
+ /**
162
+ * Send a message (must be already serialized)
163
+ * Messages are queued if not connected and sent when connection is established
164
+ */
165
+ send(message: string): Promise<void>;
166
+
167
+ /**
168
+ * Enqueue an outbound message with async preparation
169
+ * Useful when message needs async work before sending
170
+ */
171
+ enqueueOutbound(prepareMessage: () => Promise<string[]>): void;
172
+
173
+ /**
174
+ * Subscribe to messages for inbox addresses
175
+ */
176
+ subscribe(inboxAddresses: string[]): Promise<void>;
177
+
178
+ /**
179
+ * Unsubscribe from inbox addresses
180
+ */
181
+ unsubscribe(inboxAddresses: string[]): Promise<void>;
182
+
183
+ /**
184
+ * Set the message handler for incoming messages
185
+ * Only one handler can be set at a time
186
+ */
187
+ setMessageHandler(handler: MessageHandler): void;
188
+
189
+ /**
190
+ * Set the resubscribe callback (called after reconnection)
191
+ */
192
+ setResubscribeHandler(handler: () => Promise<void>): void;
193
+
194
+ /**
195
+ * Add a connection state change listener
196
+ * @returns Unsubscribe function
197
+ */
198
+ onStateChange(handler: StateChangeHandler): () => void;
199
+
200
+ /**
201
+ * Add an error listener
202
+ * @returns Unsubscribe function
203
+ */
204
+ onError(handler: ErrorHandler): () => void;
205
+ }
206
+
207
+ // ============ Factory Function Type ============
208
+
209
+ /**
210
+ * Factory function to create a WebSocket client
211
+ */
212
+ export type CreateWebSocketClient = (options: WebSocketClientOptions) => WebSocketClient;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Bookmark types for Quorum
3
+ */
4
+
5
+ export type Bookmark = {
6
+ bookmarkId: string;
7
+ messageId: string;
8
+ spaceId?: string;
9
+ channelId?: string;
10
+ conversationId?: string;
11
+ sourceType: 'channel' | 'dm';
12
+ createdAt: number;
13
+ cachedPreview: {
14
+ senderAddress: string;
15
+ senderName: string;
16
+ textSnippet: string;
17
+ messageDate: number;
18
+ sourceName: string;
19
+ contentType: 'text' | 'image' | 'sticker';
20
+ imageUrl?: string;
21
+ thumbnailUrl?: string;
22
+ stickerId?: string;
23
+ };
24
+ };
25
+
26
+ export const BOOKMARKS_CONFIG = {
27
+ MAX_BOOKMARKS: 200,
28
+ PREVIEW_SNIPPET_LENGTH: 150,
29
+ } as const;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Conversation (DM) types for Quorum
3
+ */
4
+
5
+ export type ConversationSource = 'quorum' | 'farcaster';
6
+
7
+ export type Conversation = {
8
+ conversationId: string;
9
+ type: 'direct' | 'group';
10
+ timestamp: number;
11
+ address: string;
12
+ icon: string;
13
+ displayName: string;
14
+ lastReadTimestamp?: number;
15
+ isRepudiable?: boolean;
16
+ saveEditHistory?: boolean;
17
+ lastMessageId?: string;
18
+ // Farcaster-specific fields
19
+ source?: ConversationSource; // 'quorum' (E2EE) or 'farcaster' (direct cast)
20
+ farcasterConversationId?: string; // Farcaster conversation ID (e.g., "123-456")
21
+ farcasterFid?: number; // Counterparty's Farcaster FID (for 1:1 DMs)
22
+ farcasterUsername?: string; // Counterparty's Farcaster username
23
+ farcasterParticipantFids?: number[]; // All participant FIDs except current user (for group chats)
24
+ unreadCount?: number; // Farcaster unread count
25
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @quorum/shared types
3
+ *
4
+ * All type definitions shared between mobile and desktop apps
5
+ */
6
+
7
+ // Space types
8
+ export type {
9
+ Permission,
10
+ Role,
11
+ Emoji,
12
+ Sticker,
13
+ Group,
14
+ Channel,
15
+ Space,
16
+ } from './space';
17
+
18
+ // Message types
19
+ export type {
20
+ MessageSendStatus,
21
+ PostMessage,
22
+ UpdateProfileMessage,
23
+ RemoveMessage,
24
+ EventMessage,
25
+ EmbedMessage,
26
+ ReactionMessage,
27
+ RemoveReactionMessage,
28
+ JoinMessage,
29
+ LeaveMessage,
30
+ KickMessage,
31
+ MuteMessage,
32
+ StickerMessage,
33
+ PinMessage,
34
+ DeleteConversationMessage,
35
+ EditMessage,
36
+ MessageContent,
37
+ Reaction,
38
+ Mentions,
39
+ Message,
40
+ } from './message';
41
+
42
+ // Conversation types
43
+ export type { Conversation, ConversationSource } from './conversation';
44
+
45
+ // User types
46
+ export type {
47
+ FolderColor,
48
+ NavItem,
49
+ NotificationSettings,
50
+ UserConfig,
51
+ UserProfile,
52
+ SpaceMember,
53
+ } from './user';
54
+
55
+ // Bookmark types
56
+ export type { Bookmark } from './bookmark';
57
+ export { BOOKMARKS_CONFIG } from './bookmark';
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Message-related types for Quorum
3
+ */
4
+
5
+ /** Client-side ephemeral status - NEVER persist to storage or include in network payload */
6
+ export type MessageSendStatus = 'sending' | 'sent' | 'failed';
7
+
8
+ export type PostMessage = {
9
+ senderId: string;
10
+ type: 'post';
11
+ text: string | string[];
12
+ repliesToMessageId?: string;
13
+ };
14
+
15
+ export type UpdateProfileMessage = {
16
+ senderId: string;
17
+ type: 'update-profile';
18
+ displayName: string;
19
+ userIcon: string;
20
+ };
21
+
22
+ export type RemoveMessage = {
23
+ senderId: string;
24
+ type: 'remove-message';
25
+ removeMessageId: string;
26
+ };
27
+
28
+ export type EventMessage = {
29
+ senderId: string;
30
+ type: 'event';
31
+ text: string;
32
+ repliesToMessageId?: string;
33
+ };
34
+
35
+ export type EmbedMessage = {
36
+ senderId: string;
37
+ type: 'embed';
38
+ imageUrl?: string;
39
+ thumbnailUrl?: string;
40
+ videoUrl?: string;
41
+ width?: string;
42
+ height?: string;
43
+ isLargeGif?: boolean;
44
+ repliesToMessageId?: string;
45
+ };
46
+
47
+ export type ReactionMessage = {
48
+ senderId: string;
49
+ type: 'reaction';
50
+ reaction: string;
51
+ messageId: string;
52
+ };
53
+
54
+ export type RemoveReactionMessage = {
55
+ senderId: string;
56
+ type: 'remove-reaction';
57
+ reaction: string;
58
+ messageId: string;
59
+ };
60
+
61
+ export type JoinMessage = {
62
+ senderId: string;
63
+ type: 'join';
64
+ };
65
+
66
+ export type LeaveMessage = {
67
+ senderId: string;
68
+ type: 'leave';
69
+ };
70
+
71
+ export type KickMessage = {
72
+ senderId: string;
73
+ type: 'kick';
74
+ };
75
+
76
+ export type MuteMessage = {
77
+ senderId: string;
78
+ type: 'mute';
79
+ targetUserId: string;
80
+ muteId: string;
81
+ timestamp: number;
82
+ action: 'mute' | 'unmute';
83
+ duration?: number;
84
+ };
85
+
86
+ export type StickerMessage = {
87
+ senderId: string;
88
+ type: 'sticker';
89
+ stickerId: string;
90
+ repliesToMessageId?: string;
91
+ };
92
+
93
+ export type PinMessage = {
94
+ senderId: string;
95
+ type: 'pin';
96
+ targetMessageId: string;
97
+ action: 'pin' | 'unpin';
98
+ };
99
+
100
+ export type DeleteConversationMessage = {
101
+ senderId: string;
102
+ type: 'delete-conversation';
103
+ };
104
+
105
+ export type EditMessage = {
106
+ senderId: string;
107
+ type: 'edit-message';
108
+ originalMessageId: string;
109
+ editedText: string | string[];
110
+ editedAt: number;
111
+ editNonce: string;
112
+ editSignature?: string;
113
+ };
114
+
115
+ export type MessageContent =
116
+ | PostMessage
117
+ | EventMessage
118
+ | EmbedMessage
119
+ | ReactionMessage
120
+ | RemoveReactionMessage
121
+ | RemoveMessage
122
+ | JoinMessage
123
+ | LeaveMessage
124
+ | KickMessage
125
+ | MuteMessage
126
+ | UpdateProfileMessage
127
+ | StickerMessage
128
+ | PinMessage
129
+ | DeleteConversationMessage
130
+ | EditMessage;
131
+
132
+ export type Reaction = {
133
+ emojiId: string;
134
+ spaceId: string;
135
+ emojiName: string;
136
+ count: number;
137
+ memberIds: string[];
138
+ };
139
+
140
+ export type Mentions = {
141
+ memberIds: string[];
142
+ roleIds: string[];
143
+ channelIds: string[];
144
+ everyone?: boolean;
145
+ totalMentionCount?: number;
146
+ };
147
+
148
+ export type Message = {
149
+ channelId: string;
150
+ spaceId: string;
151
+ messageId: string;
152
+ digestAlgorithm: string;
153
+ nonce: string;
154
+ createdDate: number;
155
+ modifiedDate: number;
156
+ lastModifiedHash: string;
157
+ content: MessageContent;
158
+ reactions: Reaction[];
159
+ mentions: Mentions;
160
+ replyMetadata?: {
161
+ parentAuthor: string;
162
+ parentChannelId: string;
163
+ };
164
+ publicKey?: string;
165
+ signature?: string;
166
+ isPinned?: boolean;
167
+ pinnedAt?: number;
168
+ pinnedBy?: string;
169
+ edits?: Array<{
170
+ text: string | string[];
171
+ modifiedDate: number;
172
+ lastModifiedHash: string;
173
+ }>;
174
+ /** Client-side ephemeral - NEVER persist or transmit */
175
+ sendStatus?: MessageSendStatus;
176
+ /** Client-side ephemeral - sanitized error message for display */
177
+ sendError?: string;
178
+ };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Space-related types for Quorum
3
+ */
4
+
5
+ export type Permission = 'message:delete' | 'message:pin' | 'mention:everyone' | 'user:mute';
6
+
7
+ export type Role = {
8
+ roleId: string;
9
+ displayName: string;
10
+ roleTag: string;
11
+ color: string;
12
+ members: string[];
13
+ permissions: Permission[];
14
+ isPublic?: boolean;
15
+ };
16
+
17
+ export type Emoji = {
18
+ name: string;
19
+ id: string;
20
+ imgUrl: string;
21
+ };
22
+
23
+ export type Sticker = {
24
+ name: string;
25
+ id: string;
26
+ imgUrl: string;
27
+ };
28
+
29
+ export type Group = {
30
+ groupName: string;
31
+ channels: Channel[];
32
+ icon?: string;
33
+ iconColor?: string;
34
+ iconVariant?: 'outline' | 'filled';
35
+ };
36
+
37
+ export type Channel = {
38
+ channelId: string;
39
+ spaceId: string;
40
+ channelName: string;
41
+ channelTopic: string;
42
+ channelKey?: string;
43
+ createdDate: number;
44
+ modifiedDate: number;
45
+ mentionCount?: number;
46
+ mentions?: string;
47
+ isReadOnly?: boolean;
48
+ managerRoleIds?: string[];
49
+ isPinned?: boolean;
50
+ pinnedAt?: number;
51
+ icon?: string;
52
+ iconColor?: string;
53
+ iconVariant?: 'outline' | 'filled';
54
+ };
55
+
56
+ export type Space = {
57
+ spaceId: string;
58
+ spaceName: string;
59
+ description?: string;
60
+ vanityUrl: string;
61
+ inviteUrl: string;
62
+ iconUrl: string;
63
+ bannerUrl: string;
64
+ defaultChannelId: string;
65
+ hubAddress: string;
66
+ createdDate: number;
67
+ modifiedDate: number;
68
+ isRepudiable: boolean;
69
+ isPublic: boolean;
70
+ saveEditHistory?: boolean;
71
+ groups: Group[];
72
+ roles: Role[];
73
+ emojis: Emoji[];
74
+ stickers: Sticker[];
75
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * User-related types for Quorum
3
+ */
4
+
5
+ import type { Bookmark } from './bookmark';
6
+
7
+ export type FolderColor = string;
8
+
9
+ export type NavItem =
10
+ | { type: 'space'; id: string }
11
+ | {
12
+ type: 'folder';
13
+ id: string;
14
+ name: string;
15
+ spaceIds: string[];
16
+ icon?: string;
17
+ color?: FolderColor;
18
+ createdDate: number;
19
+ modifiedDate: number;
20
+ };
21
+
22
+ export type NotificationSettings = {
23
+ enabled?: boolean;
24
+ mentions?: boolean;
25
+ replies?: boolean;
26
+ all?: boolean;
27
+ };
28
+
29
+ export type UserConfig = {
30
+ address: string;
31
+ spaceIds: string[];
32
+ items?: NavItem[];
33
+ timestamp?: number;
34
+ nonRepudiable?: boolean;
35
+ allowSync?: boolean;
36
+ name?: string;
37
+ profile_image?: string;
38
+ spaceKeys?: {
39
+ spaceId: string;
40
+ encryptionState: {
41
+ conversationId: string;
42
+ inboxId: string;
43
+ state: string;
44
+ timestamp: number;
45
+ };
46
+ keys: {
47
+ keyId: string;
48
+ address?: string;
49
+ publicKey: string;
50
+ privateKey: string;
51
+ spaceId: string;
52
+ }[];
53
+ }[];
54
+ notificationSettings?: {
55
+ [spaceId: string]: NotificationSettings;
56
+ };
57
+ bookmarks?: Bookmark[];
58
+ deletedBookmarkIds?: string[];
59
+ };
60
+
61
+ export type UserProfile = {
62
+ address: string;
63
+ name?: string;
64
+ display_name?: string;
65
+ profile_image?: string;
66
+ bio?: string;
67
+ };
68
+
69
+ export type SpaceMember = UserProfile & {
70
+ inbox_address: string;
71
+ isKicked?: boolean;
72
+ };