@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.
- package/dist/index.d.mts +2414 -0
- package/dist/index.d.ts +2414 -0
- package/dist/index.js +2788 -0
- package/dist/index.mjs +2678 -0
- package/package.json +49 -0
- package/src/api/client.ts +86 -0
- package/src/api/endpoints.ts +87 -0
- package/src/api/errors.ts +179 -0
- package/src/api/index.ts +35 -0
- package/src/crypto/encryption-state.ts +249 -0
- package/src/crypto/index.ts +55 -0
- package/src/crypto/types.ts +307 -0
- package/src/crypto/wasm-provider.ts +298 -0
- package/src/hooks/index.ts +31 -0
- package/src/hooks/keys.ts +62 -0
- package/src/hooks/mutations/index.ts +15 -0
- package/src/hooks/mutations/useDeleteMessage.ts +67 -0
- package/src/hooks/mutations/useEditMessage.ts +87 -0
- package/src/hooks/mutations/useReaction.ts +163 -0
- package/src/hooks/mutations/useSendMessage.ts +131 -0
- package/src/hooks/useChannels.ts +49 -0
- package/src/hooks/useMessages.ts +77 -0
- package/src/hooks/useSpaces.ts +60 -0
- package/src/index.ts +32 -0
- package/src/signing/index.ts +10 -0
- package/src/signing/types.ts +83 -0
- package/src/signing/wasm-provider.ts +75 -0
- package/src/storage/adapter.ts +118 -0
- package/src/storage/index.ts +9 -0
- package/src/sync/index.ts +83 -0
- package/src/sync/service.test.ts +822 -0
- package/src/sync/service.ts +947 -0
- package/src/sync/types.ts +267 -0
- package/src/sync/utils.ts +588 -0
- package/src/transport/browser-websocket.ts +299 -0
- package/src/transport/index.ts +34 -0
- package/src/transport/rn-websocket.ts +321 -0
- package/src/transport/types.ts +56 -0
- package/src/transport/websocket.ts +212 -0
- package/src/types/bookmark.ts +29 -0
- package/src/types/conversation.ts +25 -0
- package/src/types/index.ts +57 -0
- package/src/types/message.ts +178 -0
- package/src/types/space.ts +75 -0
- package/src/types/user.ts +72 -0
- package/src/utils/encoding.ts +106 -0
- package/src/utils/formatting.ts +139 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/logger.ts +141 -0
- package/src/utils/mentions.ts +135 -0
- package/src/utils/validation.ts +84 -0
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quilibrium/quorum-shared",
|
|
3
|
+
"version": "2.1.0-1",
|
|
4
|
+
"description": "Shared types, hooks, and utilities for Quorum mobile and desktop apps",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
17
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
18
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "vitest",
|
|
21
|
+
"test:run": "vitest run"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@noble/hashes": "^1.3.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@tanstack/react-query": ">=5.0.0",
|
|
28
|
+
"react": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@tanstack/react-query": "^5.62.0",
|
|
32
|
+
"@types/react": "^18.2.0",
|
|
33
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
34
|
+
"react": "^18.2.0",
|
|
35
|
+
"tsup": "^8.0.0",
|
|
36
|
+
"typescript": "^5.3.0",
|
|
37
|
+
"vitest": "^4.0.16"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"src"
|
|
42
|
+
],
|
|
43
|
+
"keywords": [
|
|
44
|
+
"quorum",
|
|
45
|
+
"chat",
|
|
46
|
+
"shared"
|
|
47
|
+
],
|
|
48
|
+
"license": "MIT"
|
|
49
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuorumApiClient interface
|
|
3
|
+
*
|
|
4
|
+
* Platform-agnostic API client that can be implemented differently
|
|
5
|
+
* for mobile (fetch) and desktop (Electron IPC or fetch)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Space, Message, Conversation } from '../types';
|
|
9
|
+
|
|
10
|
+
export interface SendMessageParams {
|
|
11
|
+
spaceId: string;
|
|
12
|
+
channelId: string;
|
|
13
|
+
text: string;
|
|
14
|
+
repliesToMessageId?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AddReactionParams {
|
|
18
|
+
spaceId: string;
|
|
19
|
+
channelId: string;
|
|
20
|
+
messageId: string;
|
|
21
|
+
reaction: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RemoveReactionParams {
|
|
25
|
+
spaceId: string;
|
|
26
|
+
channelId: string;
|
|
27
|
+
messageId: string;
|
|
28
|
+
reaction: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface EditMessageParams {
|
|
32
|
+
spaceId: string;
|
|
33
|
+
channelId: string;
|
|
34
|
+
messageId: string;
|
|
35
|
+
text: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DeleteMessageParams {
|
|
39
|
+
spaceId: string;
|
|
40
|
+
channelId: string;
|
|
41
|
+
messageId: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SendDirectMessageParams {
|
|
45
|
+
conversationId: string;
|
|
46
|
+
text: string;
|
|
47
|
+
repliesToMessageId?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface QuorumApiClient {
|
|
51
|
+
// Spaces
|
|
52
|
+
fetchSpaces(): Promise<Space[]>;
|
|
53
|
+
fetchSpace(spaceId: string): Promise<Space>;
|
|
54
|
+
joinSpace(inviteCode: string): Promise<Space>;
|
|
55
|
+
|
|
56
|
+
// Messages
|
|
57
|
+
fetchMessages(params: {
|
|
58
|
+
spaceId: string;
|
|
59
|
+
channelId: string;
|
|
60
|
+
cursor?: string;
|
|
61
|
+
limit?: number;
|
|
62
|
+
}): Promise<{ messages: Message[]; nextPageToken?: string }>;
|
|
63
|
+
|
|
64
|
+
sendMessage(params: SendMessageParams): Promise<Message>;
|
|
65
|
+
editMessage(params: EditMessageParams): Promise<Message>;
|
|
66
|
+
deleteMessage(params: DeleteMessageParams): Promise<void>;
|
|
67
|
+
|
|
68
|
+
// Reactions
|
|
69
|
+
addReaction(params: AddReactionParams): Promise<void>;
|
|
70
|
+
removeReaction(params: RemoveReactionParams): Promise<void>;
|
|
71
|
+
|
|
72
|
+
// Conversations
|
|
73
|
+
fetchConversations(): Promise<Conversation[]>;
|
|
74
|
+
createConversation(params: { address: string }): Promise<Conversation>;
|
|
75
|
+
sendDirectMessage(params: SendDirectMessageParams): Promise<Message>;
|
|
76
|
+
fetchDirectMessages(params: {
|
|
77
|
+
conversationId: string;
|
|
78
|
+
cursor?: string;
|
|
79
|
+
limit?: number;
|
|
80
|
+
}): Promise<{ messages: Message[]; nextPageToken?: string }>;
|
|
81
|
+
|
|
82
|
+
// Pinning
|
|
83
|
+
pinMessage(params: { spaceId: string; channelId: string; messageId: string }): Promise<void>;
|
|
84
|
+
unpinMessage(params: { spaceId: string; channelId: string; messageId: string }): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API endpoint definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Base API configuration */
|
|
6
|
+
export interface ApiConfig {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** API endpoint builders */
|
|
12
|
+
export const endpoints = {
|
|
13
|
+
// Spaces
|
|
14
|
+
spaces: {
|
|
15
|
+
list: () => '/spaces',
|
|
16
|
+
detail: (spaceId: string) => `/spaces/${spaceId}`,
|
|
17
|
+
members: (spaceId: string) => `/spaces/${spaceId}/members`,
|
|
18
|
+
join: (spaceId: string) => `/spaces/${spaceId}/join`,
|
|
19
|
+
leave: (spaceId: string) => `/spaces/${spaceId}/leave`,
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// Channels
|
|
23
|
+
channels: {
|
|
24
|
+
list: (spaceId: string) => `/spaces/${spaceId}/channels`,
|
|
25
|
+
detail: (spaceId: string, channelId: string) =>
|
|
26
|
+
`/spaces/${spaceId}/channels/${channelId}`,
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// Messages
|
|
30
|
+
messages: {
|
|
31
|
+
list: (spaceId: string, channelId: string) =>
|
|
32
|
+
`/spaces/${spaceId}/channels/${channelId}/messages`,
|
|
33
|
+
detail: (spaceId: string, channelId: string, messageId: string) =>
|
|
34
|
+
`/spaces/${spaceId}/channels/${channelId}/messages/${messageId}`,
|
|
35
|
+
send: (spaceId: string, channelId: string) =>
|
|
36
|
+
`/spaces/${spaceId}/channels/${channelId}/messages`,
|
|
37
|
+
react: (spaceId: string, channelId: string, messageId: string) =>
|
|
38
|
+
`/spaces/${spaceId}/channels/${channelId}/messages/${messageId}/reactions`,
|
|
39
|
+
pin: (spaceId: string, channelId: string, messageId: string) =>
|
|
40
|
+
`/spaces/${spaceId}/channels/${channelId}/messages/${messageId}/pin`,
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// Conversations (DMs)
|
|
44
|
+
conversations: {
|
|
45
|
+
list: () => '/conversations',
|
|
46
|
+
detail: (conversationId: string) => `/conversations/${conversationId}`,
|
|
47
|
+
messages: (conversationId: string) => `/conversations/${conversationId}/messages`,
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// User
|
|
51
|
+
user: {
|
|
52
|
+
config: () => '/user/config',
|
|
53
|
+
profile: () => '/user/profile',
|
|
54
|
+
notifications: () => '/user/notifications',
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Search
|
|
58
|
+
search: {
|
|
59
|
+
messages: (spaceId: string) => `/spaces/${spaceId}/search/messages`,
|
|
60
|
+
members: (spaceId: string) => `/spaces/${spaceId}/search/members`,
|
|
61
|
+
},
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
/** HTTP methods */
|
|
65
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
66
|
+
|
|
67
|
+
/** Request options */
|
|
68
|
+
export interface RequestOptions {
|
|
69
|
+
method?: HttpMethod;
|
|
70
|
+
headers?: Record<string, string>;
|
|
71
|
+
body?: unknown;
|
|
72
|
+
timeout?: number;
|
|
73
|
+
signal?: AbortSignal;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Pagination parameters */
|
|
77
|
+
export interface PaginationParams {
|
|
78
|
+
cursor?: string;
|
|
79
|
+
limit?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Message list parameters */
|
|
83
|
+
export interface MessageListParams extends PaginationParams {
|
|
84
|
+
before?: string;
|
|
85
|
+
after?: string;
|
|
86
|
+
around?: string;
|
|
87
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API error classes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** API error codes */
|
|
6
|
+
export enum ApiErrorCode {
|
|
7
|
+
// Network errors
|
|
8
|
+
NETWORK_ERROR = 'NETWORK_ERROR',
|
|
9
|
+
TIMEOUT = 'TIMEOUT',
|
|
10
|
+
|
|
11
|
+
// Auth errors
|
|
12
|
+
UNAUTHORIZED = 'UNAUTHORIZED',
|
|
13
|
+
FORBIDDEN = 'FORBIDDEN',
|
|
14
|
+
TOKEN_EXPIRED = 'TOKEN_EXPIRED',
|
|
15
|
+
|
|
16
|
+
// Validation errors
|
|
17
|
+
VALIDATION_ERROR = 'VALIDATION_ERROR',
|
|
18
|
+
BAD_REQUEST = 'BAD_REQUEST',
|
|
19
|
+
|
|
20
|
+
// Resource errors
|
|
21
|
+
NOT_FOUND = 'NOT_FOUND',
|
|
22
|
+
CONFLICT = 'CONFLICT',
|
|
23
|
+
|
|
24
|
+
// Rate limiting
|
|
25
|
+
RATE_LIMITED = 'RATE_LIMITED',
|
|
26
|
+
|
|
27
|
+
// Server errors
|
|
28
|
+
SERVER_ERROR = 'SERVER_ERROR',
|
|
29
|
+
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
|
|
30
|
+
|
|
31
|
+
// Unknown
|
|
32
|
+
UNKNOWN = 'UNKNOWN',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** API error details */
|
|
36
|
+
export interface ApiErrorDetails {
|
|
37
|
+
code: ApiErrorCode;
|
|
38
|
+
message: string;
|
|
39
|
+
status?: number;
|
|
40
|
+
field?: string;
|
|
41
|
+
retryAfter?: number;
|
|
42
|
+
originalError?: Error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Custom API error class
|
|
47
|
+
*/
|
|
48
|
+
export class ApiError extends Error {
|
|
49
|
+
readonly code: ApiErrorCode;
|
|
50
|
+
readonly status?: number;
|
|
51
|
+
readonly field?: string;
|
|
52
|
+
readonly retryAfter?: number;
|
|
53
|
+
readonly originalError?: Error;
|
|
54
|
+
|
|
55
|
+
constructor(details: ApiErrorDetails) {
|
|
56
|
+
super(details.message);
|
|
57
|
+
this.name = 'ApiError';
|
|
58
|
+
this.code = details.code;
|
|
59
|
+
this.status = details.status;
|
|
60
|
+
this.field = details.field;
|
|
61
|
+
this.retryAfter = details.retryAfter;
|
|
62
|
+
this.originalError = details.originalError;
|
|
63
|
+
|
|
64
|
+
// Maintain proper stack trace
|
|
65
|
+
if (Error.captureStackTrace) {
|
|
66
|
+
Error.captureStackTrace(this, ApiError);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Check if error is retryable */
|
|
71
|
+
get isRetryable(): boolean {
|
|
72
|
+
return [
|
|
73
|
+
ApiErrorCode.NETWORK_ERROR,
|
|
74
|
+
ApiErrorCode.TIMEOUT,
|
|
75
|
+
ApiErrorCode.RATE_LIMITED,
|
|
76
|
+
ApiErrorCode.SERVER_ERROR,
|
|
77
|
+
ApiErrorCode.SERVICE_UNAVAILABLE,
|
|
78
|
+
].includes(this.code);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Check if error requires re-authentication */
|
|
82
|
+
get requiresAuth(): boolean {
|
|
83
|
+
return [
|
|
84
|
+
ApiErrorCode.UNAUTHORIZED,
|
|
85
|
+
ApiErrorCode.TOKEN_EXPIRED,
|
|
86
|
+
].includes(this.code);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Convert to JSON-serializable object */
|
|
90
|
+
toJSON(): ApiErrorDetails {
|
|
91
|
+
return {
|
|
92
|
+
code: this.code,
|
|
93
|
+
message: this.message,
|
|
94
|
+
status: this.status,
|
|
95
|
+
field: this.field,
|
|
96
|
+
retryAfter: this.retryAfter,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create ApiError from HTTP response
|
|
103
|
+
*/
|
|
104
|
+
export function createApiError(
|
|
105
|
+
status: number,
|
|
106
|
+
message?: string,
|
|
107
|
+
field?: string
|
|
108
|
+
): ApiError {
|
|
109
|
+
let code: ApiErrorCode;
|
|
110
|
+
let defaultMessage: string;
|
|
111
|
+
|
|
112
|
+
switch (status) {
|
|
113
|
+
case 400:
|
|
114
|
+
code = ApiErrorCode.BAD_REQUEST;
|
|
115
|
+
defaultMessage = 'Invalid request';
|
|
116
|
+
break;
|
|
117
|
+
case 401:
|
|
118
|
+
code = ApiErrorCode.UNAUTHORIZED;
|
|
119
|
+
defaultMessage = 'Authentication required';
|
|
120
|
+
break;
|
|
121
|
+
case 403:
|
|
122
|
+
code = ApiErrorCode.FORBIDDEN;
|
|
123
|
+
defaultMessage = 'Access denied';
|
|
124
|
+
break;
|
|
125
|
+
case 404:
|
|
126
|
+
code = ApiErrorCode.NOT_FOUND;
|
|
127
|
+
defaultMessage = 'Resource not found';
|
|
128
|
+
break;
|
|
129
|
+
case 409:
|
|
130
|
+
code = ApiErrorCode.CONFLICT;
|
|
131
|
+
defaultMessage = 'Resource conflict';
|
|
132
|
+
break;
|
|
133
|
+
case 422:
|
|
134
|
+
code = ApiErrorCode.VALIDATION_ERROR;
|
|
135
|
+
defaultMessage = 'Validation failed';
|
|
136
|
+
break;
|
|
137
|
+
case 429:
|
|
138
|
+
code = ApiErrorCode.RATE_LIMITED;
|
|
139
|
+
defaultMessage = 'Rate limit exceeded';
|
|
140
|
+
break;
|
|
141
|
+
case 500:
|
|
142
|
+
code = ApiErrorCode.SERVER_ERROR;
|
|
143
|
+
defaultMessage = 'Server error';
|
|
144
|
+
break;
|
|
145
|
+
case 503:
|
|
146
|
+
code = ApiErrorCode.SERVICE_UNAVAILABLE;
|
|
147
|
+
defaultMessage = 'Service unavailable';
|
|
148
|
+
break;
|
|
149
|
+
default:
|
|
150
|
+
code = ApiErrorCode.UNKNOWN;
|
|
151
|
+
defaultMessage = 'An unexpected error occurred';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return new ApiError({
|
|
155
|
+
code,
|
|
156
|
+
message: message || defaultMessage,
|
|
157
|
+
status,
|
|
158
|
+
field,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Create ApiError from network error
|
|
164
|
+
*/
|
|
165
|
+
export function createNetworkError(error: Error): ApiError {
|
|
166
|
+
if (error.name === 'AbortError') {
|
|
167
|
+
return new ApiError({
|
|
168
|
+
code: ApiErrorCode.TIMEOUT,
|
|
169
|
+
message: 'Request timed out',
|
|
170
|
+
originalError: error,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return new ApiError({
|
|
175
|
+
code: ApiErrorCode.NETWORK_ERROR,
|
|
176
|
+
message: 'Network error',
|
|
177
|
+
originalError: error,
|
|
178
|
+
});
|
|
179
|
+
}
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Client interface and types
|
|
6
|
+
export type {
|
|
7
|
+
QuorumApiClient,
|
|
8
|
+
SendMessageParams,
|
|
9
|
+
SendDirectMessageParams,
|
|
10
|
+
AddReactionParams,
|
|
11
|
+
RemoveReactionParams,
|
|
12
|
+
EditMessageParams,
|
|
13
|
+
DeleteMessageParams,
|
|
14
|
+
} from './client';
|
|
15
|
+
|
|
16
|
+
// Errors
|
|
17
|
+
export {
|
|
18
|
+
ApiError,
|
|
19
|
+
ApiErrorCode,
|
|
20
|
+
createApiError,
|
|
21
|
+
createNetworkError,
|
|
22
|
+
} from './errors';
|
|
23
|
+
export type { ApiErrorDetails } from './errors';
|
|
24
|
+
|
|
25
|
+
// Endpoints
|
|
26
|
+
export {
|
|
27
|
+
endpoints,
|
|
28
|
+
} from './endpoints';
|
|
29
|
+
export type {
|
|
30
|
+
ApiConfig,
|
|
31
|
+
HttpMethod,
|
|
32
|
+
RequestOptions,
|
|
33
|
+
PaginationParams,
|
|
34
|
+
MessageListParams,
|
|
35
|
+
} from './endpoints';
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption State Types and Storage Interface
|
|
3
|
+
*
|
|
4
|
+
* Platform-agnostic types for managing Double Ratchet encryption states.
|
|
5
|
+
* The actual storage implementation is platform-specific (MMKV for mobile, IndexedDB for desktop).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============ Device & Recipient Types ============
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* DeviceKeys - Device's cryptographic keypairs for E2E encryption
|
|
12
|
+
* Used for X3DH key exchange and inbox message unsealing
|
|
13
|
+
*/
|
|
14
|
+
export interface DeviceKeys {
|
|
15
|
+
/** X448 identity key for X3DH - private key */
|
|
16
|
+
identityPrivateKey: number[];
|
|
17
|
+
/** X448 identity key for X3DH - public key */
|
|
18
|
+
identityPublicKey: number[];
|
|
19
|
+
/** X448 signed pre-key for X3DH - private key */
|
|
20
|
+
preKeyPrivateKey: number[];
|
|
21
|
+
/** X448 signed pre-key for X3DH - public key */
|
|
22
|
+
preKeyPublicKey: number[];
|
|
23
|
+
/** X448 inbox encryption key for unsealing - private key */
|
|
24
|
+
inboxEncryptionPrivateKey: number[];
|
|
25
|
+
/** X448 inbox encryption key for unsealing - public key */
|
|
26
|
+
inboxEncryptionPublicKey: number[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* RecipientInfo - Recipient's public keys needed for encryption
|
|
31
|
+
* Used to establish X3DH session and seal messages
|
|
32
|
+
*/
|
|
33
|
+
export interface RecipientInfo {
|
|
34
|
+
/** Recipient's address */
|
|
35
|
+
address: string;
|
|
36
|
+
/** X448 identity public key for X3DH */
|
|
37
|
+
identityKey: number[];
|
|
38
|
+
/** X448 signed pre-key for X3DH */
|
|
39
|
+
signedPreKey: number[];
|
|
40
|
+
/** Recipient's inbox address */
|
|
41
|
+
inboxAddress: string;
|
|
42
|
+
/** X448 public key for sealing envelopes to recipient's inbox */
|
|
43
|
+
inboxEncryptionKey?: number[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* EncryptedEnvelope - Result of encrypting a message
|
|
48
|
+
*/
|
|
49
|
+
export interface EncryptedEnvelope {
|
|
50
|
+
/** The encrypted Double Ratchet envelope */
|
|
51
|
+
envelope: string;
|
|
52
|
+
/** Recipient's inbox address */
|
|
53
|
+
inboxAddress: string;
|
|
54
|
+
/** Ephemeral public key (only set for first message in new session) */
|
|
55
|
+
ephemeralPublicKey?: number[];
|
|
56
|
+
/** Ephemeral private key (only set for first message - needed for sealing) */
|
|
57
|
+
ephemeralPrivateKey?: number[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ============ Storage Provider Interface ============
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* KeyValueStorageProvider - Platform-agnostic key-value storage interface
|
|
64
|
+
*
|
|
65
|
+
* Implementations:
|
|
66
|
+
* - MMKV (React Native)
|
|
67
|
+
* - IndexedDB (Desktop/Web)
|
|
68
|
+
* - AsyncStorage (React Native fallback)
|
|
69
|
+
*/
|
|
70
|
+
export interface KeyValueStorageProvider {
|
|
71
|
+
/**
|
|
72
|
+
* Get a string value by key
|
|
73
|
+
* @returns The value or null if not found
|
|
74
|
+
*/
|
|
75
|
+
getString(key: string): string | null;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Set a string value
|
|
79
|
+
*/
|
|
80
|
+
set(key: string, value: string): void;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Remove a key
|
|
84
|
+
*/
|
|
85
|
+
remove(key: string): void;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get all keys in storage
|
|
89
|
+
*/
|
|
90
|
+
getAllKeys(): string[];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Clear all data from storage
|
|
94
|
+
*/
|
|
95
|
+
clearAll(): void;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============ Inbox Types ============
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* SendingInbox - Recipient's inbox info needed for sealing messages
|
|
102
|
+
* Matches desktop SDK's SendingInbox type
|
|
103
|
+
*/
|
|
104
|
+
export interface SendingInbox {
|
|
105
|
+
/** Recipient's inbox address where we send to */
|
|
106
|
+
inbox_address: string;
|
|
107
|
+
/** Recipient's X448 public key for sealing (hex) */
|
|
108
|
+
inbox_encryption_key: string;
|
|
109
|
+
/** Recipient's Ed448 public key (hex) - empty until confirmed */
|
|
110
|
+
inbox_public_key: string;
|
|
111
|
+
/** Always empty - we don't have their private key */
|
|
112
|
+
inbox_private_key: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* ReceivingInbox - Our inbox info for receiving replies
|
|
117
|
+
* Simplified version (full keypair stored separately in ConversationInboxKeypair)
|
|
118
|
+
*/
|
|
119
|
+
export interface ReceivingInbox {
|
|
120
|
+
/** Our inbox address where we receive replies */
|
|
121
|
+
inbox_address: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============ Encryption State ============
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* EncryptionState - Double Ratchet state for a conversation+inbox pair
|
|
128
|
+
* Matches desktop's DoubleRatchetStateAndInboxKeys structure
|
|
129
|
+
*/
|
|
130
|
+
export interface EncryptionState {
|
|
131
|
+
/** JSON-serialized ratchet state */
|
|
132
|
+
state: string;
|
|
133
|
+
/** When state was created/updated */
|
|
134
|
+
timestamp: number;
|
|
135
|
+
/** Conversation identifier */
|
|
136
|
+
conversationId: string;
|
|
137
|
+
/** Associated inbox ID (our receiving inbox) */
|
|
138
|
+
inboxId: string;
|
|
139
|
+
/** Whether we've sent an accept message */
|
|
140
|
+
sentAccept?: boolean;
|
|
141
|
+
/** Recipient's inbox info for sealing messages */
|
|
142
|
+
sendingInbox?: SendingInbox;
|
|
143
|
+
/** Session tag (usually our inbox address) */
|
|
144
|
+
tag?: string;
|
|
145
|
+
/**
|
|
146
|
+
* X3DH ephemeral public key (hex) used for session establishment.
|
|
147
|
+
* MUST be reused for all init envelopes until session is confirmed.
|
|
148
|
+
* This ensures the receiver can derive the same session key via X3DH.
|
|
149
|
+
*/
|
|
150
|
+
x3dhEphemeralPublicKey?: string;
|
|
151
|
+
/**
|
|
152
|
+
* X3DH ephemeral private key (hex) used for session establishment.
|
|
153
|
+
* MUST be reused for sealing init envelopes until session is confirmed.
|
|
154
|
+
*/
|
|
155
|
+
x3dhEphemeralPrivateKey?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============ Inbox Mapping ============
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* InboxMapping - Maps inbox address to conversation
|
|
162
|
+
*/
|
|
163
|
+
export interface InboxMapping {
|
|
164
|
+
inboxId: string;
|
|
165
|
+
conversationId: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* LatestState - Tracks the most recent state for a conversation
|
|
170
|
+
*/
|
|
171
|
+
export interface LatestState {
|
|
172
|
+
conversationId: string;
|
|
173
|
+
inboxId: string;
|
|
174
|
+
timestamp: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============ Conversation Inbox Keypair ============
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* ConversationInboxKeypair - Per-conversation inbox keypair for receiving replies
|
|
181
|
+
* Mirrors desktop's InboxKeyset structure with both Ed448 and X448 keys
|
|
182
|
+
*/
|
|
183
|
+
export interface ConversationInboxKeypair {
|
|
184
|
+
conversationId: string;
|
|
185
|
+
inboxAddress: string;
|
|
186
|
+
/** X448 encryption public key (for sealing/unsealing messages) */
|
|
187
|
+
encryptionPublicKey: number[];
|
|
188
|
+
/** X448 encryption private key */
|
|
189
|
+
encryptionPrivateKey: number[];
|
|
190
|
+
/** Ed448 signing public key (for signing/verifying inbox messages) */
|
|
191
|
+
signingPublicKey?: number[];
|
|
192
|
+
/** Ed448 signing private key */
|
|
193
|
+
signingPrivateKey?: number[];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============ Storage Key Prefixes ============
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Storage key prefixes for encryption state data
|
|
200
|
+
*/
|
|
201
|
+
export const ENCRYPTION_STORAGE_KEYS = {
|
|
202
|
+
/** enc_state:{conversationId}:{inboxId} */
|
|
203
|
+
ENCRYPTION_STATE: 'enc_state:',
|
|
204
|
+
/** inbox_map:{inboxId} */
|
|
205
|
+
INBOX_MAPPING: 'inbox_map:',
|
|
206
|
+
/** latest:{conversationId} */
|
|
207
|
+
LATEST_STATE: 'latest:',
|
|
208
|
+
/** conv_inboxes:{conversationId} -> inboxId[] */
|
|
209
|
+
CONVERSATION_INBOXES: 'conv_inboxes:',
|
|
210
|
+
/** conv_inbox_key:{conversationId} -> ConversationInboxKeypair */
|
|
211
|
+
CONVERSATION_INBOX_KEY: 'conv_inbox_key:',
|
|
212
|
+
} as const;
|
|
213
|
+
|
|
214
|
+
// ============ Encryption State Storage Interface ============
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* EncryptionStateStorageInterface - Interface for managing encryption states
|
|
218
|
+
*
|
|
219
|
+
* This interface defines the contract for encryption state storage.
|
|
220
|
+
* Implementations use platform-specific storage backends.
|
|
221
|
+
*/
|
|
222
|
+
export interface EncryptionStateStorageInterface {
|
|
223
|
+
// Encryption States
|
|
224
|
+
getEncryptionState(conversationId: string, inboxId: string): EncryptionState | null;
|
|
225
|
+
getEncryptionStates(conversationId: string): EncryptionState[];
|
|
226
|
+
saveEncryptionState(state: EncryptionState, updateLatest?: boolean): void;
|
|
227
|
+
deleteEncryptionState(conversationId: string, inboxId: string): void;
|
|
228
|
+
deleteAllEncryptionStates(conversationId: string): void;
|
|
229
|
+
|
|
230
|
+
// Inbox Mapping
|
|
231
|
+
getInboxMapping(inboxId: string): InboxMapping | null;
|
|
232
|
+
saveInboxMapping(inboxId: string, conversationId: string): void;
|
|
233
|
+
deleteInboxMapping(inboxId: string): void;
|
|
234
|
+
|
|
235
|
+
// Latest State
|
|
236
|
+
getLatestState(conversationId: string): LatestState | null;
|
|
237
|
+
|
|
238
|
+
// Conversation Inbox Keypairs
|
|
239
|
+
saveConversationInboxKeypair(keypair: ConversationInboxKeypair): void;
|
|
240
|
+
getConversationInboxKeypair(conversationId: string): ConversationInboxKeypair | null;
|
|
241
|
+
deleteConversationInboxKeypair(conversationId: string): void;
|
|
242
|
+
getConversationInboxKeypairByAddress(inboxAddress: string): ConversationInboxKeypair | null;
|
|
243
|
+
getAllConversationInboxAddresses(): string[];
|
|
244
|
+
|
|
245
|
+
// Utilities
|
|
246
|
+
clearAll(): void;
|
|
247
|
+
hasEncryptionState(conversationId: string): boolean;
|
|
248
|
+
getStatesByInboxId(inboxId: string): Array<{ conversationId: string; state: EncryptionState }>;
|
|
249
|
+
}
|