@droponair/sdk-js 0.29.0 → 0.31.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 CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.31.0
4
+
5
+ ### Fixed
6
+
7
+ - **People on different platforms could not send to each other.** Public keys are
8
+ published to a shared directory, and the SDKs disagreed on how to write them:
9
+ one used the standard base64 alphabet and another the URL-safe one. Each could
10
+ read what it had written and none could read the others, so encrypting for a
11
+ member on another platform failed outright with a decoding error, and the send
12
+ failed for the sender rather than for that member.
13
+ - Keys are now decoded in either alphabet, with or without padding, so a
14
+ directory shared between platforms works whatever wrote the entry.
15
+
16
+ ### Notes
17
+
18
+ - Nothing needs republishing: existing entries in either form are readable.
19
+ - This affects any app whose users are on more than one platform.
20
+
21
+ ## 0.30.0
22
+
23
+ ### Added
24
+
25
+ - **Typing indicators.** `sendTyping(conversationId, isTyping)` and `onTyping(callback)`. Nothing in the platform carried this before, so an
26
+ app that wanted to show someone composing had nowhere to send it.
27
+ - Typing is never stored. It describes this moment and is worth nothing after it,
28
+ so it reaches only those connected at the time and is never replayed. Treat a
29
+ start as expiring on its own after a few seconds rather than waiting for the
30
+ matching stop, which is lost if the sender disconnects mid-sentence.
31
+ - Gated separately from read receipts, which name a message that was read and are
32
+ opted into per group. Typing says only that somebody is composing and discloses
33
+ nothing about what, so it needs no per-group opt-in.
34
+
3
35
  ## 0.29.0
4
36
 
5
37
  ### Fixed
package/README.md CHANGED
@@ -380,6 +380,22 @@ on `SERVER_RECEIVED`, which is about the relay rather than any member.
380
380
  was closed or disconnected is drained automatically when it reconnects and delivered
381
381
  through `onGroupMessage` just like a live message. There is nothing to call.
382
382
 
383
+ ### Typing indicators
384
+
385
+ Say when someone starts and stops composing, and hear it about others. Nothing is
386
+ stored: it reaches only those connected at the time and is never replayed, so
387
+ treat a start as expiring on its own rather than waiting for the stop, which is
388
+ lost if the sender disconnects mid-sentence.
389
+
390
+ ```typescript
391
+ client.sendTyping(groupId, true);
392
+ client.sendTyping(groupId, false);
393
+
394
+ client.onTyping((event) => {
395
+ // event.conversationId, event.fromUserId, event.isTyping
396
+ });
397
+ ```
398
+
383
399
  ### 1-to-1 Calls
384
400
 
385
401
  | Method | Returns | Description |
@@ -1,6 +1,14 @@
1
1
  export declare function utf8Encode(value: string): Uint8Array;
2
2
  export declare function utf8Decode(value: Uint8Array): string;
3
3
  export declare function toBase64(value: Uint8Array): string;
4
+ /**
5
+ * Decodes base64 in either alphabet, with or without padding.
6
+ *
7
+ * Values reaching an SDK are written by whatever device produced them, and the
8
+ * SDKs did not agree on which alphabet to use: each could read its own and none
9
+ * could read the others, so two people on different platforms could not send to
10
+ * each other at all. `atob` is strict, so the input is normalised first.
11
+ */
4
12
  export declare function fromBase64(value: string): Uint8Array;
5
13
  export declare function randomBytes(length: number): Uint8Array;
6
14
  export declare function concatBytes(...parts: Uint8Array[]): Uint8Array;
@@ -24,11 +24,23 @@ function toBase64(value) {
24
24
  }
25
25
  return btoa(binary);
26
26
  }
27
+ /**
28
+ * Decodes base64 in either alphabet, with or without padding.
29
+ *
30
+ * Values reaching an SDK are written by whatever device produced them, and the
31
+ * SDKs did not agree on which alphabet to use: each could read its own and none
32
+ * could read the others, so two people on different platforms could not send to
33
+ * each other at all. `atob` is strict, so the input is normalised first.
34
+ */
27
35
  function fromBase64(value) {
36
+ const normalised = value.replace(/-/g, '+').replace(/_/g, '/');
37
+ const padded = normalised.length % 4 === 0
38
+ ? normalised
39
+ : normalised + '='.repeat(4 - (normalised.length % 4));
28
40
  if (typeof Buffer !== 'undefined') {
29
- return new Uint8Array(Buffer.from(value, 'base64'));
41
+ return new Uint8Array(Buffer.from(padded, 'base64'));
30
42
  }
31
- const binary = atob(value);
43
+ const binary = atob(padded);
32
44
  const out = new Uint8Array(binary.length);
33
45
  for (let i = 0; i < binary.length; i += 1) {
34
46
  out[i] = binary.charCodeAt(i);
@@ -1,6 +1,6 @@
1
1
  import { CryptoService } from '../crypto/crypto-service';
2
2
  import { SessionManager } from './session-manager';
3
- import { BroadcastCallback, CallEventCallback, DropOnAirClient, EventCallback, CreateRoomOptions, GroupCallEventCallback, GroupInfo, GroupMessageCallback, Room, UpdateRoomOptions, SfuToken, SfuRecording, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo, ReadReceiptCallback, NotificationClearCallback, DraftSyncCallback, KeyCustody, PushPayload } from './types';
3
+ import { BroadcastCallback, CallEventCallback, DropOnAirClient, EventCallback, CreateRoomOptions, GroupCallEventCallback, GroupInfo, GroupMessageCallback, Room, UpdateRoomOptions, SfuToken, SfuRecording, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo, ReadReceiptCallback, TypingCallback, NotificationClearCallback, DraftSyncCallback, KeyCustody, PushPayload } from './types';
4
4
  import { AttachmentRef, CreateUploadSessionOptions, DownloadedAttachment, PrepareAttachmentOptions, UploadSession } from '../attachment/attachment-types';
5
5
  export declare class MessagingClient implements DropOnAirClient {
6
6
  private readonly options;
@@ -90,6 +90,7 @@ export declare class MessagingClient implements DropOnAirClient {
90
90
  private readonly broadcastListeners;
91
91
  private readonly messageEditListeners;
92
92
  private readonly readReceiptListeners;
93
+ private readonly typingListeners;
93
94
  private readonly notificationClearListeners;
94
95
  private readonly draftSyncListeners;
95
96
  private readonly messageDeleteListeners;
@@ -146,6 +147,11 @@ export declare class MessagingClient implements DropOnAirClient {
146
147
  * reported. Fires when another device of the same user calls markRead();
147
148
  * use it to clear the unread state your app maintains for that message.
148
149
  */
150
+ /**
151
+ * Listen for someone starting or stopping typing in a conversation. Nothing is
152
+ * stored, so this only fires while both sides are connected.
153
+ */
154
+ onTyping(callback: TypingCallback): () => void;
149
155
  onReadReceipt(callback: ReadReceiptCallback): () => void;
150
156
  editMessage(originalMessageId: string, toUserId: string, newPlaintext: string): Promise<{
151
157
  editId: string;
@@ -217,6 +223,14 @@ export declare class MessagingClient implements DropOnAirClient {
217
223
  * @param conversationId optional peer userId / group id, echoed back to
218
224
  * other devices so they can bucket the receipt
219
225
  */
226
+ /**
227
+ * Say this user started or stopped composing in a conversation.
228
+ *
229
+ * Never stored, so it reaches only those connected at the time. Send `false`
230
+ * when they stop, but treat the indicator as expiring on its own: if the sender
231
+ * disconnects mid-sentence, the stop never arrives.
232
+ */
233
+ sendTyping(conversationId: string, isTyping: boolean): void;
220
234
  markRead(messageId: string, conversationId?: string): void;
221
235
  /**
222
236
  * Tell this user's other devices that the notification(s) for a
@@ -282,6 +282,7 @@ class MessagingClient {
282
282
  this.broadcastListeners = new Set();
283
283
  this.messageEditListeners = new Set();
284
284
  this.readReceiptListeners = new Set();
285
+ this.typingListeners = new Set();
285
286
  this.notificationClearListeners = new Set();
286
287
  this.draftSyncListeners = new Set();
287
288
  this.messageDeleteListeners = new Set();
@@ -519,6 +520,14 @@ class MessagingClient {
519
520
  * reported. Fires when another device of the same user calls markRead();
520
521
  * use it to clear the unread state your app maintains for that message.
521
522
  */
523
+ /**
524
+ * Listen for someone starting or stopping typing in a conversation. Nothing is
525
+ * stored, so this only fires while both sides are connected.
526
+ */
527
+ onTyping(callback) {
528
+ this.typingListeners.add(callback);
529
+ return () => this.typingListeners.delete(callback);
530
+ }
522
531
  onReadReceipt(callback) {
523
532
  this.readReceiptListeners.add(callback);
524
533
  return () => this.readReceiptListeners.delete(callback);
@@ -777,6 +786,26 @@ class MessagingClient {
777
786
  * @param conversationId optional peer userId / group id, echoed back to
778
787
  * other devices so they can bucket the receipt
779
788
  */
789
+ /**
790
+ * Say this user started or stopped composing in a conversation.
791
+ *
792
+ * Never stored, so it reaches only those connected at the time. Send `false`
793
+ * when they stop, but treat the indicator as expiring on its own: if the sender
794
+ * disconnects mid-sentence, the stop never arrives.
795
+ */
796
+ sendTyping(conversationId, isTyping) {
797
+ if (!this.transport?.isOpen()) {
798
+ return;
799
+ }
800
+ const frame = {
801
+ type: 'SYNC_TYPING',
802
+ messageId: '',
803
+ conversationId,
804
+ timestamp: Date.now(),
805
+ payload: isTyping ? 'start' : 'stop',
806
+ };
807
+ this.transport.send(this.codec.encodeSyncFrame(frame));
808
+ }
780
809
  markRead(messageId, conversationId) {
781
810
  if (!this.transport?.isOpen()) {
782
811
  throw new Error('DropOnAir websocket is not connected');
@@ -838,7 +867,24 @@ class MessagingClient {
838
867
  return () => this.draftSyncListeners.delete(callback);
839
868
  }
840
869
  handleIncomingSync(frame) {
841
- if (frame.type === 'SYNC_READ_RECEIPT') {
870
+ if (frame.type === 'SYNC_TYPING') {
871
+ if (!frame.conversationId || !frame.fromUserId) {
872
+ return;
873
+ }
874
+ const event = {
875
+ conversationId: frame.conversationId,
876
+ fromUserId: frame.fromUserId,
877
+ isTyping: frame.payload !== 'stop',
878
+ timestamp: frame.timestamp,
879
+ };
880
+ for (const listener of this.typingListeners) {
881
+ try {
882
+ listener(event);
883
+ }
884
+ catch { /* listener errors must not break the socket */ }
885
+ }
886
+ }
887
+ else if (frame.type === 'SYNC_READ_RECEIPT') {
842
888
  const event = {
843
889
  messageId: frame.messageId,
844
890
  conversationId: frame.conversationId || undefined,
@@ -109,6 +109,24 @@ export interface ReadReceiptEvent {
109
109
  fromUserId?: string;
110
110
  }
111
111
  export type ReadReceiptCallback = (event: ReadReceiptEvent) => void;
112
+ /**
113
+ * Someone in a conversation started or stopped composing.
114
+ *
115
+ * Ephemeral by design: the platform never stores it, so it arrives only while
116
+ * both sides are connected and nothing is replayed afterwards. Treat a `true` as
117
+ * expiring on its own after a few seconds rather than waiting for the matching
118
+ * `false`, which is lost if the sender disconnects mid-sentence.
119
+ */
120
+ export interface TypingEvent {
121
+ /** The group or peer the typing is happening in. */
122
+ conversationId: string;
123
+ /** Who is typing. */
124
+ fromUserId: string;
125
+ /** Whether they started or stopped. */
126
+ isTyping: boolean;
127
+ timestamp: number;
128
+ }
129
+ export type TypingCallback = (event: TypingEvent) => void;
112
130
  /**
113
131
  * Delivered to the user's OTHER devices when one device calls
114
132
  * clearNotification(). Use it to clear the conversation's notification badge.
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@ export { SDK_VERSION, PROTOCOL_VERSION, PAYLOAD_FORMAT_VERSION } from './version
22
22
  */
23
23
  export declare function createSecureIdentity(store?: IdentityRecordStore): Promise<WebCryptoIdentityProvider | null>;
24
24
  export declare function initialize(options: InitializeOptions): Promise<DropOnAirClient>;
25
- export type { InitializeOptions, DropOnAirClient, DropOnAirEvent, MessageCallback, EventCallback, DecryptedMessage, KeyStorageAdapter, CallEvent, CallEventType, CallEventCallback, TurnCredentials, BroadcastMessage, BroadcastCallback, GroupInfo, GroupMemberInfo, DecryptedGroupMessage, GroupMessageCallback, GroupCallEvent, GroupCallEventType, GroupCallEventCallback, Room, RoomPolicy, CreateRoomOptions, UpdateRoomOptions, SfuToken, SfuRecording, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, DeviceInfo, ReadReceiptEvent, ReadReceiptCallback, NotificationClearEvent, NotificationClearCallback, DraftSyncEvent, DraftSyncCallback, } from './core/types';
25
+ export type { InitializeOptions, DropOnAirClient, DropOnAirEvent, MessageCallback, EventCallback, DecryptedMessage, KeyStorageAdapter, CallEvent, CallEventType, CallEventCallback, TurnCredentials, BroadcastMessage, BroadcastCallback, GroupInfo, GroupMemberInfo, DecryptedGroupMessage, GroupMessageCallback, GroupCallEvent, GroupCallEventType, GroupCallEventCallback, Room, RoomPolicy, CreateRoomOptions, UpdateRoomOptions, SfuToken, SfuRecording, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, DeviceInfo, ReadReceiptEvent, ReadReceiptCallback, TypingEvent, TypingCallback, NotificationClearEvent, NotificationClearCallback, DraftSyncEvent, DraftSyncCallback, } from './core/types';
26
26
  export type { AttachmentEncryptionType, AttachmentConversationType, AttachmentRef, DeviceWrappedKey, CreateUploadSessionOptions, UploadSession, PrepareAttachmentOptions, DownloadedAttachment, } from './attachment/attachment-types';
27
27
  export { SseTransport, type SseTransportOptions, type SseFrameHandler, type SseStateHandler, } from './transport/sse-transport';
28
28
  export { WebTransportTransport, type WebTransportTransportOptions, type WTFrameHandler, type WTStateHandler, } from './transport/webtransport-transport';
package/dist/version.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * MINOR, additive feature (e.g. multi-device payloads, new call event type)
8
8
  * PATCH, bug-fix / perf improvement with no wire or API change
9
9
  */
10
- export declare const SDK_VERSION = "0.29.0";
10
+ export declare const SDK_VERSION = "0.31.0";
11
11
  /**
12
12
  * Binary encrypted-payload format version.
13
13
  * Included as the first byte of every encrypted payload so receivers can
package/dist/version.js CHANGED
@@ -10,7 +10,7 @@ exports.PROTOCOL_VERSION = exports.PAYLOAD_FORMAT_VERSION = exports.SDK_VERSION
10
10
  * MINOR, additive feature (e.g. multi-device payloads, new call event type)
11
11
  * PATCH, bug-fix / perf improvement with no wire or API change
12
12
  */
13
- exports.SDK_VERSION = '0.29.0';
13
+ exports.SDK_VERSION = '0.31.0';
14
14
  /**
15
15
  * Binary encrypted-payload format version.
16
16
  * Included as the first byte of every encrypted payload so receivers can
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@droponair/sdk-js",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "description": "End-to-end encrypted messaging, voice and video calling SDK. The relay never sees your keys or message content.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",