@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,106 @@
1
+ /**
2
+ * Encoding utilities for hex and base64 conversions
3
+ *
4
+ * These are platform-agnostic utilities for converting between
5
+ * byte arrays and string representations.
6
+ */
7
+
8
+ /**
9
+ * Convert a hex string to a byte array
10
+ * @param hex - Hexadecimal string (with or without 0x prefix)
11
+ * @returns Array of bytes
12
+ */
13
+ export function hexToBytes(hex: string): number[] {
14
+ // Remove 0x prefix if present
15
+ const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex;
16
+ const bytes: number[] = [];
17
+ for (let i = 0; i < cleanHex.length; i += 2) {
18
+ bytes.push(parseInt(cleanHex.substr(i, 2), 16));
19
+ }
20
+ return bytes;
21
+ }
22
+
23
+ /**
24
+ * Convert a byte array to a hex string
25
+ * @param bytes - Array of bytes (number[] or Uint8Array)
26
+ * @returns Hexadecimal string (lowercase, no prefix)
27
+ */
28
+ export function bytesToHex(bytes: number[] | Uint8Array): string {
29
+ return Array.from(bytes)
30
+ .map((b) => b.toString(16).padStart(2, '0'))
31
+ .join('');
32
+ }
33
+
34
+ /**
35
+ * Convert a base64 string to a Uint8Array
36
+ * @param base64 - Base64-encoded string
37
+ * @returns Uint8Array of bytes
38
+ */
39
+ export function base64ToBytes(base64: string): Uint8Array {
40
+ // Handle both browser and Node.js environments
41
+ if (typeof atob === 'function') {
42
+ // Browser environment
43
+ const binaryString = atob(base64);
44
+ const bytes = new Uint8Array(binaryString.length);
45
+ for (let i = 0; i < binaryString.length; i++) {
46
+ bytes[i] = binaryString.charCodeAt(i);
47
+ }
48
+ return bytes;
49
+ } else {
50
+ // Node.js environment
51
+ return new Uint8Array(Buffer.from(base64, 'base64'));
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Convert a byte array to a base64 string
57
+ * @param bytes - Array of bytes (number[] or Uint8Array)
58
+ * @returns Base64-encoded string
59
+ */
60
+ export function bytesToBase64(bytes: number[] | Uint8Array): string {
61
+ // Handle both browser and Node.js environments
62
+ if (typeof btoa === 'function') {
63
+ // Browser environment
64
+ const binaryString = Array.from(bytes)
65
+ .map((b) => String.fromCharCode(b))
66
+ .join('');
67
+ return btoa(binaryString);
68
+ } else {
69
+ // Node.js environment
70
+ return Buffer.from(bytes).toString('base64');
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Convert a UTF-8 string to a byte array
76
+ * @param str - UTF-8 string
77
+ * @returns Array of bytes
78
+ */
79
+ export function stringToBytes(str: string): number[] {
80
+ const encoder = new TextEncoder();
81
+ return Array.from(encoder.encode(str));
82
+ }
83
+
84
+ /**
85
+ * Convert a byte array to a UTF-8 string
86
+ * @param bytes - Array of bytes (number[] or Uint8Array)
87
+ * @returns UTF-8 string
88
+ */
89
+ export function bytesToString(bytes: number[] | Uint8Array): string {
90
+ const decoder = new TextDecoder();
91
+ return decoder.decode(new Uint8Array(bytes));
92
+ }
93
+
94
+ /**
95
+ * Convert a number to an 8-byte big-endian array (int64)
96
+ * Used for timestamp serialization in signatures
97
+ *
98
+ * @param num - The number to convert (must be within safe integer range)
99
+ * @returns 8-byte Uint8Array in big-endian format
100
+ */
101
+ export function int64ToBytes(num: number): Uint8Array {
102
+ const arr = new Uint8Array(8);
103
+ const view = new DataView(arr.buffer);
104
+ view.setBigInt64(0, BigInt(num), false);
105
+ return arr;
106
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Date and text formatting utilities
3
+ */
4
+
5
+ /**
6
+ * Format timestamp to time string (e.g., "2:30 PM")
7
+ */
8
+ export function formatTime(timestamp: number): string {
9
+ const date = new Date(timestamp);
10
+ return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
11
+ }
12
+
13
+ /**
14
+ * Format timestamp to date string (e.g., "Dec 24, 2025")
15
+ */
16
+ export function formatDate(timestamp: number): string {
17
+ const date = new Date(timestamp);
18
+ return date.toLocaleDateString([], {
19
+ month: 'short',
20
+ day: 'numeric',
21
+ year: 'numeric',
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Format timestamp to full datetime string (e.g., "Dec 24, 2025 at 2:30 PM")
27
+ */
28
+ export function formatDateTime(timestamp: number): string {
29
+ const date = new Date(timestamp);
30
+ return date.toLocaleString([], {
31
+ month: 'short',
32
+ day: 'numeric',
33
+ year: 'numeric',
34
+ hour: 'numeric',
35
+ minute: '2-digit',
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Format timestamp to relative time (e.g., "5 minutes ago", "Yesterday")
41
+ */
42
+ export function formatRelativeTime(timestamp: number): string {
43
+ const now = Date.now();
44
+ const diff = now - timestamp;
45
+
46
+ const seconds = Math.floor(diff / 1000);
47
+ const minutes = Math.floor(seconds / 60);
48
+ const hours = Math.floor(minutes / 60);
49
+ const days = Math.floor(hours / 24);
50
+
51
+ if (seconds < 60) {
52
+ return 'Just now';
53
+ }
54
+ if (minutes < 60) {
55
+ return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
56
+ }
57
+ if (hours < 24) {
58
+ return `${hours} hour${hours === 1 ? '' : 's'} ago`;
59
+ }
60
+ if (days === 1) {
61
+ return 'Yesterday';
62
+ }
63
+ if (days < 7) {
64
+ return `${days} days ago`;
65
+ }
66
+
67
+ return formatDate(timestamp);
68
+ }
69
+
70
+ /**
71
+ * Format message date header (e.g., "Today", "Yesterday", "December 24, 2025")
72
+ */
73
+ export function formatMessageDate(timestamp: number): string {
74
+ const date = new Date(timestamp);
75
+ const today = new Date();
76
+ const yesterday = new Date(today);
77
+ yesterday.setDate(yesterday.getDate() - 1);
78
+
79
+ if (isSameDay(date, today)) {
80
+ return 'Today';
81
+ }
82
+ if (isSameDay(date, yesterday)) {
83
+ return 'Yesterday';
84
+ }
85
+
86
+ return date.toLocaleDateString([], {
87
+ weekday: 'long',
88
+ month: 'long',
89
+ day: 'numeric',
90
+ year: date.getFullYear() !== today.getFullYear() ? 'numeric' : undefined,
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Check if two dates are the same day
96
+ */
97
+ export function isSameDay(d1: Date, d2: Date): boolean {
98
+ return (
99
+ d1.getFullYear() === d2.getFullYear() &&
100
+ d1.getMonth() === d2.getMonth() &&
101
+ d1.getDate() === d2.getDate()
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Truncate text to a maximum length with ellipsis
107
+ */
108
+ export function truncateText(text: string, maxLength: number): string {
109
+ if (text.length <= maxLength) {
110
+ return text;
111
+ }
112
+ return text.slice(0, maxLength - 1) + '\u2026';
113
+ }
114
+
115
+ /**
116
+ * Format file size (e.g., "1.5 MB")
117
+ */
118
+ export function formatFileSize(bytes: number): string {
119
+ if (bytes === 0) return '0 B';
120
+
121
+ const k = 1024;
122
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
123
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
124
+
125
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
126
+ }
127
+
128
+ /**
129
+ * Format member count (e.g., "1.2K members")
130
+ */
131
+ export function formatMemberCount(count: number): string {
132
+ if (count < 1000) {
133
+ return `${count} member${count === 1 ? '' : 's'}`;
134
+ }
135
+ if (count < 1000000) {
136
+ return `${(count / 1000).toFixed(1)}K members`;
137
+ }
138
+ return `${(count / 1000000).toFixed(1)}M members`;
139
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Utility exports
3
+ */
4
+
5
+ export * from './validation';
6
+ export * from './mentions';
7
+ export * from './formatting';
8
+ export * from './encoding';
9
+ export * from './logger';
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Logger utility that respects build environment
3
+ *
4
+ * In development: logs to console
5
+ * In production: no-ops for performance
6
+ *
7
+ * Usage:
8
+ * import { logger } from '@quorum/shared';
9
+ * logger.log('[MyModule]', 'some message', data);
10
+ * logger.warn('[MyModule]', 'warning message');
11
+ * logger.error('[MyModule]', 'error message', error);
12
+ */
13
+
14
+ type LogLevel = 'log' | 'info' | 'warn' | 'error' | 'debug';
15
+
16
+ interface LoggerConfig {
17
+ enabled: boolean;
18
+ minLevel: LogLevel;
19
+ }
20
+
21
+ const LOG_LEVELS: Record<LogLevel, number> = {
22
+ debug: 0,
23
+ log: 1,
24
+ info: 2,
25
+ warn: 3,
26
+ error: 4,
27
+ };
28
+
29
+ // Default config - can be overridden by calling logger.configure()
30
+ let config: LoggerConfig = {
31
+ enabled: true, // Will be set based on environment
32
+ minLevel: 'log',
33
+ };
34
+
35
+ // Detect environment
36
+ function detectEnvironment(): boolean {
37
+ // React Native / Expo
38
+ if (typeof __DEV__ !== 'undefined') {
39
+ return __DEV__;
40
+ }
41
+ // Node.js / Electron
42
+ if (typeof process !== 'undefined' && process.env) {
43
+ return process.env.NODE_ENV !== 'production';
44
+ }
45
+ // Browser
46
+ if (typeof window !== 'undefined') {
47
+ return window.location?.hostname === 'localhost';
48
+ }
49
+ // Default to enabled
50
+ return true;
51
+ }
52
+
53
+ // Initialize based on environment
54
+ config.enabled = detectEnvironment();
55
+
56
+ // No-op function for production
57
+ const noop = (): void => {};
58
+
59
+ function shouldLog(level: LogLevel): boolean {
60
+ if (!config.enabled) return false;
61
+ return LOG_LEVELS[level] >= LOG_LEVELS[config.minLevel];
62
+ }
63
+
64
+ function createLogMethod(level: LogLevel): (...args: unknown[]) => void {
65
+ return (...args: unknown[]) => {
66
+ if (shouldLog(level)) {
67
+ console[level](...args);
68
+ }
69
+ };
70
+ }
71
+
72
+ export const logger = {
73
+ /**
74
+ * Configure the logger
75
+ */
76
+ configure(newConfig: Partial<LoggerConfig>): void {
77
+ config = { ...config, ...newConfig };
78
+ },
79
+
80
+ /**
81
+ * Check if logging is enabled
82
+ */
83
+ isEnabled(): boolean {
84
+ return config.enabled;
85
+ },
86
+
87
+ /**
88
+ * Enable logging (useful for debugging production issues)
89
+ */
90
+ enable(): void {
91
+ config.enabled = true;
92
+ },
93
+
94
+ /**
95
+ * Disable logging
96
+ */
97
+ disable(): void {
98
+ config.enabled = false;
99
+ },
100
+
101
+ /**
102
+ * Log at debug level
103
+ */
104
+ debug: createLogMethod('debug'),
105
+
106
+ /**
107
+ * Log at default level
108
+ */
109
+ log: createLogMethod('log'),
110
+
111
+ /**
112
+ * Log at info level
113
+ */
114
+ info: createLogMethod('info'),
115
+
116
+ /**
117
+ * Log at warn level
118
+ */
119
+ warn: createLogMethod('warn'),
120
+
121
+ /**
122
+ * Log at error level (always logs unless explicitly disabled)
123
+ */
124
+ error: createLogMethod('error'),
125
+
126
+ /**
127
+ * Create a scoped logger with a prefix
128
+ */
129
+ scope(prefix: string) {
130
+ return {
131
+ debug: (...args: unknown[]) => logger.debug(prefix, ...args),
132
+ log: (...args: unknown[]) => logger.log(prefix, ...args),
133
+ info: (...args: unknown[]) => logger.info(prefix, ...args),
134
+ warn: (...args: unknown[]) => logger.warn(prefix, ...args),
135
+ error: (...args: unknown[]) => logger.error(prefix, ...args),
136
+ };
137
+ },
138
+ };
139
+
140
+ // Type declaration for React Native's __DEV__ global
141
+ declare const __DEV__: boolean | undefined;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Mention parsing utilities
3
+ */
4
+
5
+ import type { Mentions } from '../types';
6
+
7
+ /** Mention patterns */
8
+ export const MENTION_PATTERNS = {
9
+ user: /<@([a-zA-Z0-9]+)>/g,
10
+ role: /<@&([a-zA-Z0-9]+)>/g,
11
+ channel: /<#([a-zA-Z0-9]+)>/g,
12
+ everyone: /@everyone/g,
13
+ here: /@here/g,
14
+ };
15
+
16
+ /** Parsed mention */
17
+ export interface ParsedMention {
18
+ type: 'user' | 'role' | 'channel' | 'everyone' | 'here';
19
+ id?: string;
20
+ raw: string;
21
+ start: number;
22
+ end: number;
23
+ }
24
+
25
+ /**
26
+ * Parse mentions from message text
27
+ */
28
+ export function parseMentions(text: string): ParsedMention[] {
29
+ const mentions: ParsedMention[] = [];
30
+
31
+ // Parse user mentions
32
+ let match: RegExpExecArray | null;
33
+ const userRegex = new RegExp(MENTION_PATTERNS.user.source, 'g');
34
+ while ((match = userRegex.exec(text)) !== null) {
35
+ mentions.push({
36
+ type: 'user',
37
+ id: match[1],
38
+ raw: match[0],
39
+ start: match.index,
40
+ end: match.index + match[0].length,
41
+ });
42
+ }
43
+
44
+ // Parse role mentions
45
+ const roleRegex = new RegExp(MENTION_PATTERNS.role.source, 'g');
46
+ while ((match = roleRegex.exec(text)) !== null) {
47
+ mentions.push({
48
+ type: 'role',
49
+ id: match[1],
50
+ raw: match[0],
51
+ start: match.index,
52
+ end: match.index + match[0].length,
53
+ });
54
+ }
55
+
56
+ // Parse channel mentions
57
+ const channelRegex = new RegExp(MENTION_PATTERNS.channel.source, 'g');
58
+ while ((match = channelRegex.exec(text)) !== null) {
59
+ mentions.push({
60
+ type: 'channel',
61
+ id: match[1],
62
+ raw: match[0],
63
+ start: match.index,
64
+ end: match.index + match[0].length,
65
+ });
66
+ }
67
+
68
+ // Parse @everyone
69
+ const everyoneRegex = new RegExp(MENTION_PATTERNS.everyone.source, 'g');
70
+ while ((match = everyoneRegex.exec(text)) !== null) {
71
+ mentions.push({
72
+ type: 'everyone',
73
+ raw: match[0],
74
+ start: match.index,
75
+ end: match.index + match[0].length,
76
+ });
77
+ }
78
+
79
+ // Parse @here
80
+ const hereRegex = new RegExp(MENTION_PATTERNS.here.source, 'g');
81
+ while ((match = hereRegex.exec(text)) !== null) {
82
+ mentions.push({
83
+ type: 'here',
84
+ raw: match[0],
85
+ start: match.index,
86
+ end: match.index + match[0].length,
87
+ });
88
+ }
89
+
90
+ // Sort by position
91
+ return mentions.sort((a, b) => a.start - b.start);
92
+ }
93
+
94
+ /**
95
+ * Extract Mentions object from parsed mentions
96
+ */
97
+ export function extractMentions(text: string): Mentions {
98
+ const parsed = parseMentions(text);
99
+
100
+ const memberIds = parsed
101
+ .filter((m) => m.type === 'user' && m.id)
102
+ .map((m) => m.id!);
103
+
104
+ const roleIds = parsed
105
+ .filter((m) => m.type === 'role' && m.id)
106
+ .map((m) => m.id!);
107
+
108
+ const channelIds = parsed
109
+ .filter((m) => m.type === 'channel' && m.id)
110
+ .map((m) => m.id!);
111
+
112
+ const everyone = parsed.some((m) => m.type === 'everyone' || m.type === 'here');
113
+
114
+ return {
115
+ memberIds: [...new Set(memberIds)],
116
+ roleIds: [...new Set(roleIds)],
117
+ channelIds: [...new Set(channelIds)],
118
+ everyone,
119
+ totalMentionCount: memberIds.length + roleIds.length + channelIds.length,
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Format mention for display
125
+ */
126
+ export function formatMention(type: 'user' | 'role' | 'channel', id: string): string {
127
+ switch (type) {
128
+ case 'user':
129
+ return `<@${id}>`;
130
+ case 'role':
131
+ return `<@&${id}>`;
132
+ case 'channel':
133
+ return `<#${id}>`;
134
+ }
135
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Message validation utilities
3
+ */
4
+
5
+ import type { Message, PostMessage } from '../types';
6
+
7
+ /** Maximum message length */
8
+ export const MAX_MESSAGE_LENGTH = 4000;
9
+
10
+ /** Maximum number of mentions per message */
11
+ export const MAX_MENTIONS = 50;
12
+
13
+ /** Validation result */
14
+ export interface ValidationResult {
15
+ valid: boolean;
16
+ errors: string[];
17
+ }
18
+
19
+ /**
20
+ * Validate message content before sending
21
+ */
22
+ export function validateMessageContent(content: string): ValidationResult {
23
+ const errors: string[] = [];
24
+
25
+ if (!content || content.trim().length === 0) {
26
+ errors.push('Message cannot be empty');
27
+ }
28
+
29
+ if (content.length > MAX_MESSAGE_LENGTH) {
30
+ errors.push(`Message exceeds maximum length of ${MAX_MESSAGE_LENGTH} characters`);
31
+ }
32
+
33
+ return {
34
+ valid: errors.length === 0,
35
+ errors,
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Validate a message object
41
+ */
42
+ export function validateMessage(message: Partial<Message>): ValidationResult {
43
+ const errors: string[] = [];
44
+
45
+ if (!message.messageId) {
46
+ errors.push('Message ID is required');
47
+ }
48
+
49
+ if (!message.channelId) {
50
+ errors.push('Channel ID is required');
51
+ }
52
+
53
+ if (!message.spaceId) {
54
+ errors.push('Space ID is required');
55
+ }
56
+
57
+ if (!message.content) {
58
+ errors.push('Message content is required');
59
+ }
60
+
61
+ if (message.content?.type === 'post') {
62
+ const postContent = message.content as PostMessage;
63
+ const text = Array.isArray(postContent.text)
64
+ ? postContent.text.join('')
65
+ : postContent.text;
66
+
67
+ if (text.length > MAX_MESSAGE_LENGTH) {
68
+ errors.push(`Message exceeds maximum length of ${MAX_MESSAGE_LENGTH} characters`);
69
+ }
70
+ }
71
+
72
+ return {
73
+ valid: errors.length === 0,
74
+ errors,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Sanitize message content for display
80
+ */
81
+ export function sanitizeContent(content: string): string {
82
+ // Remove null bytes and control characters (except newlines/tabs)
83
+ return content.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
84
+ }