@droponair/sdk-js 0.3.0
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/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +865 -0
- package/dist/core/bytes.d.ts +8 -0
- package/dist/core/bytes.js +75 -0
- package/dist/core/messaging-client.d.ts +159 -0
- package/dist/core/messaging-client.js +1293 -0
- package/dist/core/session-manager.d.ts +6 -0
- package/dist/core/session-manager.js +18 -0
- package/dist/core/types.d.ts +173 -0
- package/dist/core/types.js +2 -0
- package/dist/crypto/crypto-service.d.ts +28 -0
- package/dist/crypto/crypto-service.js +100 -0
- package/dist/crypto/payload-format.d.ts +8 -0
- package/dist/crypto/payload-format.js +63 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +29 -0
- package/dist/storage/indexeddb-key-storage.d.ts +9 -0
- package/dist/storage/indexeddb-key-storage.js +70 -0
- package/dist/storage/memory-key-storage.d.ts +7 -0
- package/dist/storage/memory-key-storage.js +18 -0
- package/dist/transport/protobuf-codec.d.ts +138 -0
- package/dist/transport/protobuf-codec.js +337 -0
- package/dist/version.d.ts +22 -0
- package/dist/version.js +25 -0
- package/package.json +49 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function utf8Encode(value: string): Uint8Array;
|
|
2
|
+
export declare function utf8Decode(value: Uint8Array): string;
|
|
3
|
+
export declare function toBase64(value: Uint8Array): string;
|
|
4
|
+
export declare function fromBase64(value: string): Uint8Array;
|
|
5
|
+
export declare function randomBytes(length: number): Uint8Array;
|
|
6
|
+
export declare function concatBytes(...parts: Uint8Array[]): Uint8Array;
|
|
7
|
+
export declare function isJwtExpired(jwt: string): boolean;
|
|
8
|
+
export declare function parseJwtPayload(jwt: string): Record<string, unknown>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.utf8Encode = utf8Encode;
|
|
4
|
+
exports.utf8Decode = utf8Decode;
|
|
5
|
+
exports.toBase64 = toBase64;
|
|
6
|
+
exports.fromBase64 = fromBase64;
|
|
7
|
+
exports.randomBytes = randomBytes;
|
|
8
|
+
exports.concatBytes = concatBytes;
|
|
9
|
+
exports.isJwtExpired = isJwtExpired;
|
|
10
|
+
exports.parseJwtPayload = parseJwtPayload;
|
|
11
|
+
function utf8Encode(value) {
|
|
12
|
+
return new TextEncoder().encode(value);
|
|
13
|
+
}
|
|
14
|
+
function utf8Decode(value) {
|
|
15
|
+
return new TextDecoder().decode(value);
|
|
16
|
+
}
|
|
17
|
+
function toBase64(value) {
|
|
18
|
+
if (typeof Buffer !== 'undefined') {
|
|
19
|
+
return Buffer.from(value).toString('base64');
|
|
20
|
+
}
|
|
21
|
+
let binary = '';
|
|
22
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
23
|
+
binary += String.fromCharCode(value[i]);
|
|
24
|
+
}
|
|
25
|
+
return btoa(binary);
|
|
26
|
+
}
|
|
27
|
+
function fromBase64(value) {
|
|
28
|
+
if (typeof Buffer !== 'undefined') {
|
|
29
|
+
return new Uint8Array(Buffer.from(value, 'base64'));
|
|
30
|
+
}
|
|
31
|
+
const binary = atob(value);
|
|
32
|
+
const out = new Uint8Array(binary.length);
|
|
33
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
34
|
+
out[i] = binary.charCodeAt(i);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function randomBytes(length) {
|
|
39
|
+
const arr = new Uint8Array(length);
|
|
40
|
+
crypto.getRandomValues(arr);
|
|
41
|
+
return arr;
|
|
42
|
+
}
|
|
43
|
+
function concatBytes(...parts) {
|
|
44
|
+
const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
|
|
45
|
+
const out = new Uint8Array(totalLength);
|
|
46
|
+
let offset = 0;
|
|
47
|
+
for (const part of parts) {
|
|
48
|
+
out.set(part, offset);
|
|
49
|
+
offset += part.length;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function isJwtExpired(jwt) {
|
|
54
|
+
const payload = parseJwtPayload(jwt);
|
|
55
|
+
if (typeof payload.exp !== 'number') {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
59
|
+
return payload.exp <= nowSeconds;
|
|
60
|
+
}
|
|
61
|
+
function parseJwtPayload(jwt) {
|
|
62
|
+
const parts = jwt.split('.');
|
|
63
|
+
if (parts.length < 2) {
|
|
64
|
+
throw new Error('Invalid JWT format');
|
|
65
|
+
}
|
|
66
|
+
const payloadPart = parts[1]
|
|
67
|
+
.replace(/-/g, '+')
|
|
68
|
+
.replace(/_/g, '/');
|
|
69
|
+
let padded = payloadPart;
|
|
70
|
+
while (padded.length % 4 !== 0) {
|
|
71
|
+
padded += '=';
|
|
72
|
+
}
|
|
73
|
+
const payloadBytes = fromBase64(padded);
|
|
74
|
+
return JSON.parse(utf8Decode(payloadBytes));
|
|
75
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { CryptoService } from '../crypto/crypto-service';
|
|
2
|
+
import { SessionManager } from './session-manager';
|
|
3
|
+
import { BroadcastCallback, CallEventCallback, DropOnAirClient, EventCallback, GroupCallEventCallback, GroupInfo, GroupMessageCallback, InitializeOptions, MessageCallback, TurnCredentials } from './types';
|
|
4
|
+
export declare class MessagingClient implements DropOnAirClient {
|
|
5
|
+
private readonly options;
|
|
6
|
+
private readonly cryptoService;
|
|
7
|
+
private readonly sessionManager;
|
|
8
|
+
private readonly storage;
|
|
9
|
+
private readonly wsUrl;
|
|
10
|
+
private readonly httpUrl;
|
|
11
|
+
private readonly tokenExchangeEndpoint;
|
|
12
|
+
private readonly keyDirectoryEndpoint;
|
|
13
|
+
private readonly fetchFn;
|
|
14
|
+
private readonly codec;
|
|
15
|
+
private ws;
|
|
16
|
+
private shouldReconnect;
|
|
17
|
+
private reconnectTimer;
|
|
18
|
+
private dropOnAirJwt;
|
|
19
|
+
private currentUserId;
|
|
20
|
+
private deviceId;
|
|
21
|
+
private rateLimited;
|
|
22
|
+
private reconnectAttempt;
|
|
23
|
+
private handlingJwtExpiry;
|
|
24
|
+
private proactiveRefreshTimer;
|
|
25
|
+
private visibilityChangeHandler;
|
|
26
|
+
private readonly autoAckIncomingMessages;
|
|
27
|
+
private static readonly DEVICE_KEYS_CACHE_TTL_MS;
|
|
28
|
+
private readonly deviceKeysCache;
|
|
29
|
+
private readonly callListeners;
|
|
30
|
+
/** Pending startCall resolver, only one outgoing call can be in-flight at a time. */
|
|
31
|
+
private pendingInviteResolve;
|
|
32
|
+
private pendingInviteReject;
|
|
33
|
+
private readonly groupMessageListeners;
|
|
34
|
+
private readonly groupCallListeners;
|
|
35
|
+
/** Pending startGroupCall resolver. */
|
|
36
|
+
private pendingGroupInviteResolve;
|
|
37
|
+
private pendingGroupInviteReject;
|
|
38
|
+
private reconnectDelayMs;
|
|
39
|
+
/**
|
|
40
|
+
* Register a document visibilitychange listener so that when the iOS app
|
|
41
|
+
* returns from background we can immediately assess the JWT state.
|
|
42
|
+
*
|
|
43
|
+
* Background problem: iOS WebView suspends JavaScript (including all
|
|
44
|
+
* setTimeout timers) while the app is backgrounded. A proactive-refresh
|
|
45
|
+
* timer scheduled for t+810 s effectively stops ticking, so if the user
|
|
46
|
+
* leaves the app open for 15+ minutes the JWT expires before the timer
|
|
47
|
+
* fires. This listener re-examines the JWT as soon as the user brings the
|
|
48
|
+
* app back to the foreground and either:
|
|
49
|
+
* a) reschedules the timer with the corrected remaining time, or
|
|
50
|
+
* b) triggers an immediate reconnect if the token has already expired or
|
|
51
|
+
* will expire within the next 90 seconds.
|
|
52
|
+
*/
|
|
53
|
+
private registerVisibilityChangeHandler;
|
|
54
|
+
private unregisterVisibilityChangeHandler;
|
|
55
|
+
/**
|
|
56
|
+
* Called every time the app returns to the foreground.
|
|
57
|
+
* Decides whether to reconnect immediately or just reschedule the proactive
|
|
58
|
+
* refresh timer with the time remaining from the current JWT's expiry.
|
|
59
|
+
*/
|
|
60
|
+
private onAppForeground;
|
|
61
|
+
/**
|
|
62
|
+
* Schedule a proactive token rotation 90 seconds before the JWT expires.
|
|
63
|
+
* When it fires, a fresh token is pre-fetched and the current WebSocket is
|
|
64
|
+
* closed cleanly so the reconnect loop picks up the new token immediately.
|
|
65
|
+
* This prevents the server from ever rejecting a message due to an expired JWT.
|
|
66
|
+
*/
|
|
67
|
+
private scheduleProactiveTokenRefresh;
|
|
68
|
+
private readonly messageListeners;
|
|
69
|
+
private readonly eventListeners;
|
|
70
|
+
private readonly broadcastListeners;
|
|
71
|
+
/** ------------------------------------------------------------------
|
|
72
|
+
* Lightweight structured logger. Only active when options.debug === true.
|
|
73
|
+
* Fields are safe to log: no plaintext, no private keys, no full JWTs.
|
|
74
|
+
* ------------------------------------------------------------------ */
|
|
75
|
+
private log;
|
|
76
|
+
private logError;
|
|
77
|
+
/**
|
|
78
|
+
* Build the common headers required on every backend API call.
|
|
79
|
+
* X-Platform and X-API-Version are forwarded when supplied in InitializeOptions.
|
|
80
|
+
*/
|
|
81
|
+
private backendHeaders;
|
|
82
|
+
/** Redact a JWT to keep subject + expiry visible while hiding the signature. */
|
|
83
|
+
private jwtSummary;
|
|
84
|
+
constructor(options: InitializeOptions, cryptoService: CryptoService, sessionManager: SessionManager, storage: import('./types').KeyStorageAdapter);
|
|
85
|
+
connect(): Promise<void>;
|
|
86
|
+
disconnect(): void;
|
|
87
|
+
sendMessage(toUserId: string, plaintextMessage: string): Promise<{
|
|
88
|
+
messageId: string;
|
|
89
|
+
}>;
|
|
90
|
+
ack(messageId: string): Promise<void>;
|
|
91
|
+
onMessage(callback: MessageCallback): () => void;
|
|
92
|
+
onEvent(callback: EventCallback): () => void;
|
|
93
|
+
sendCleartextMessage(toUserId: string, plaintext: string): Promise<{
|
|
94
|
+
messageId: string;
|
|
95
|
+
}>;
|
|
96
|
+
subscribeBroadcast(channelId: string): Promise<void>;
|
|
97
|
+
unsubscribeBroadcast(channelId: string): Promise<void>;
|
|
98
|
+
publishBroadcast(channelId: string, plaintext: string): Promise<{
|
|
99
|
+
broadcastId: string;
|
|
100
|
+
}>;
|
|
101
|
+
onBroadcast(callback: BroadcastCallback): () => void;
|
|
102
|
+
onCallEvent(callback: CallEventCallback): () => void;
|
|
103
|
+
createGroup(name: string, memberUserIds?: string[]): Promise<GroupInfo>;
|
|
104
|
+
listGroups(): Promise<GroupInfo[]>;
|
|
105
|
+
getGroup(groupId: string): Promise<GroupInfo>;
|
|
106
|
+
addGroupMembers(groupId: string, userIds: string[]): Promise<GroupInfo>;
|
|
107
|
+
removeGroupMember(groupId: string, userId: string): Promise<GroupInfo>;
|
|
108
|
+
deleteGroup(groupId: string): Promise<void>;
|
|
109
|
+
sendGroupMessage(groupId: string, plaintext: string): Promise<{
|
|
110
|
+
messageId: string;
|
|
111
|
+
}>;
|
|
112
|
+
onGroupMessage(callback: GroupMessageCallback): () => void;
|
|
113
|
+
startGroupCall(groupId: string): Promise<string>;
|
|
114
|
+
joinGroupCall(callId: string, groupId: string): Promise<void>;
|
|
115
|
+
leaveGroupCall(callId: string): Promise<void>;
|
|
116
|
+
endGroupCall(callId: string): Promise<void>;
|
|
117
|
+
sendGroupCallSignal(type: 'GROUP_CALL_SDP_OFFER' | 'GROUP_CALL_SDP_ANSWER' | 'GROUP_CALL_ICE_CANDIDATE', callId: string, groupId: string, targetUserId: string, payload: string): void;
|
|
118
|
+
onGroupCallEvent(callback: GroupCallEventCallback): () => void;
|
|
119
|
+
private sendGroupCallFrame;
|
|
120
|
+
/**
|
|
121
|
+
* Initiate an outgoing call.
|
|
122
|
+
* Sends CALL_INVITE to the server; resolves with the server-assigned callId
|
|
123
|
+
* once the server echoes back CALL_RINGING (which contains the real callId).
|
|
124
|
+
*/
|
|
125
|
+
startCall(targetUserId: string): Promise<string>;
|
|
126
|
+
acceptCall(callId: string): Promise<void>;
|
|
127
|
+
rejectCall(callId: string): Promise<void>;
|
|
128
|
+
endCall(callId: string): Promise<void>;
|
|
129
|
+
toggleVideo(callId: string, enabled: boolean): void;
|
|
130
|
+
sendCallSignal(type: 'CALL_SDP_OFFER' | 'CALL_SDP_ANSWER' | 'CALL_ICE_CANDIDATE', callId: string, payload: string): void;
|
|
131
|
+
fetchTurnCredentials(): Promise<TurnCredentials>;
|
|
132
|
+
private sendCallFrame;
|
|
133
|
+
private emitCallEvent;
|
|
134
|
+
private emitEvent;
|
|
135
|
+
private emitMessage;
|
|
136
|
+
private emitBroadcast;
|
|
137
|
+
private handleIncomingGroupMessage;
|
|
138
|
+
private emitGroupCallEvent;
|
|
139
|
+
private connectWebSocket;
|
|
140
|
+
private handleIncomingEnvelope;
|
|
141
|
+
private getPeerSharedKey;
|
|
142
|
+
/** Get or create a persistent device UUID for this SDK instance. */
|
|
143
|
+
private getOrCreateDeviceId;
|
|
144
|
+
/**
|
|
145
|
+
* Fetch all device keys for a given user (cached with short TTL).
|
|
146
|
+
* Falls back to wrapping the legacy single key into a device key entry.
|
|
147
|
+
*/
|
|
148
|
+
private fetchDeviceKeys;
|
|
149
|
+
/**
|
|
150
|
+
* Fetch my own device keys and filter out the current device.
|
|
151
|
+
* Used for self-sync: encrypt sent messages for my other devices.
|
|
152
|
+
*/
|
|
153
|
+
private fetchMyOtherDeviceKeys;
|
|
154
|
+
private ensureIdentityPublished;
|
|
155
|
+
private fetchAndProcessOfflineMessages;
|
|
156
|
+
private getValidDropOnAirJwt;
|
|
157
|
+
private extractSubject;
|
|
158
|
+
private isJwtExpiringSoon;
|
|
159
|
+
}
|