@droponair/sdk-js 0.7.0 → 0.9.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
@@ -6,6 +6,43 @@ This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.9.0], 2026-05-19
10
+
11
+ ### Added
12
+
13
+ - **Push notification token registration.** New `client.registerPushToken({ platform, token, voipToken? })` and `client.unregisterPushToken({ platform })` methods. The platform delivers a push via APNs (iOS), FCM v1 (Android), or VAPID/Web Push (browser) whenever a sender supplies a `pushPayload` on a message and the recipient device has no live WebSocket session. The token is opaque to the server. For iOS VoIP/CallKit pushes, pass `voipToken` alongside the regular APNs token.
14
+ - **PROTOCOL_VERSION bumped to 5.** Additive only: new optional `pushPayload` field on `Envelope`, `GroupEnvelope`, `CallFrame`, `GroupCallFrame`; new `PushRegistrationFrame` for token registration. Existing 0.8.x clients keep working against the new server (proto3 ignores unknown fields).
15
+
16
+ ### Notes
17
+
18
+ - E2EE invariant preserved: the push body is sender-supplied cleartext metadata only ("1 new message from Alice"), never the encrypted message contents. The recipient SDK decrypts the real message after the push wakes the device and the WebSocket reconnects.
19
+ - Customers configure their own APNs (.p8 + Team/Key/Bundle IDs), FCM service-account JSON, and VAPID keypair via the dashboard. Per-app, not per-environment; use two apps for dev/prod separation.
20
+
21
+ ---
22
+
23
+ ## [0.8.0], 2026-05-19
24
+
25
+ ### BREAKING
26
+
27
+ - `sendGroupMessage(groupId, plaintext)` is **renamed to `sendCleartextGroupMessage(groupId, plaintext)`**. The previous JS method was cleartext-only despite the name, while the Android and iOS SDKs reserved the same name for an end-to-end encrypted send. This release aligns all three SDKs on the same naming.
28
+ - **Update existing JS callers**: replace `client.sendGroupMessage(groupId, text)` with `client.sendCleartextGroupMessage(groupId, text)`.
29
+
30
+ ### Added
31
+
32
+ - **End-to-end encrypted group send.** New `sendGroupMessage(groupId, plaintext, memberUserIds, options?)` that mirrors the Android and iOS SDKs: the SDK encrypts the plaintext per-recipient-per-device (sender-side fan-out) before sending. Pass `options.attachments` for E2EE group attachments.
33
+ - **Group attachments.** Both `sendGroupMessage` and the wire `GroupEnvelope` now carry an optional `attachments` field; previously this was 1:1 only on JS.
34
+
35
+ ### Fixed
36
+
37
+ - **JS `GroupEnvelope` / `GroupMessageNotification` protobuf field numbers** now match the source-of-truth `messaging.proto`. The previous JS schema had a field-number drift versus the Android/iOS generated proto bindings, which meant group sends from JS arrived at the server with values in the wrong fields (and were silently tolerated by proto3's unknown-field handling). With this release JS group send/receive is wire-correct.
38
+
39
+ ### Notes
40
+
41
+ - Wire protocol unchanged (PROTOCOL_VERSION stays 4). The proto schema was already correct; only the JS hand-rolled codec was out of sync.
42
+ - `protoFieldNumbers` for the group types are now documented in `droponair-sdk-shared/test-vectors/protobuf-vectors.json` for cross-platform reference.
43
+
44
+ ---
45
+
9
46
  ## [0.7.0], 2026-05-19
10
47
 
11
48
  ### Added
package/README.md CHANGED
@@ -96,6 +96,31 @@ const client = await initialize(options);
96
96
  | `onMessageDelete(callback)` | `() => void` | Listen for inbound delete tombstones for 1:1 messages |
97
97
  | `onEvent(callback)` | `() => void` | Listen for system events (CONNECTED, DELIVERED, ERROR, etc.) |
98
98
  | `ack(messageId)` | `Promise<void>` | Manually acknowledge a message |
99
+ | `registerPushToken({ platform, token, voipToken? })` | `Promise<void>` | Register this device for push notifications. `platform` is `'APNS'`, `'FCM'`, or `'WEB_PUSH'`. |
100
+ | `unregisterPushToken({ platform })` | `Promise<void>` | Unregister this device for push notifications (e.g. on logout). |
101
+
102
+ ### Push notifications
103
+
104
+ Available since SDK `0.9.0`. Customers configure their own APNs / FCM / VAPID credentials in the dashboard. The platform delivers a push only when the sender attaches a `pushPayload` to a message and the recipient device has no live WebSocket session. See the per-product availability and limits on your dashboard's Subscription page.
105
+
106
+ ```typescript
107
+ // iOS, after didRegisterForRemoteNotificationsWithDeviceToken
108
+ await client.registerPushToken({ platform: 'APNS', token: deviceTokenHex });
109
+
110
+ // iOS + PushKit/CallKit (VoIP)
111
+ await client.registerPushToken({ platform: 'APNS', token: deviceTokenHex, voipToken: voipTokenHex });
112
+
113
+ // Android, after FirebaseMessaging.getInstance().token
114
+ await client.registerPushToken({ platform: 'FCM', token: fcmToken });
115
+
116
+ // Browser, the token is the JSON returned by PushManager.subscribe()
117
+ await client.registerPushToken({ platform: 'WEB_PUSH', token: JSON.stringify(subscription) });
118
+
119
+ // On logout
120
+ await client.unregisterPushToken({ platform: 'APNS' });
121
+ ```
122
+
123
+ E2EE invariant: the push body is sender-supplied cleartext metadata only (e.g. "1 new message from Alice"), never the encrypted message contents. The recipient SDK decrypts the real message after the push wakes the device and the WebSocket reconnects.
99
124
 
100
125
  ### Message Edit & Delete
101
126
 
@@ -182,7 +207,8 @@ client.onMessage(async (msg) => {
182
207
  | `addGroupMembers(groupId, userIds)` | `Promise<GroupInfo>` | Add members (owner/admin) |
183
208
  | `removeGroupMember(groupId, userId)` | `Promise<GroupInfo>` | Remove a member (owner/admin) |
184
209
  | `deleteGroup(groupId)` | `Promise<void>` | Delete a group (owner only) |
185
- | `sendGroupMessage(groupId, plaintext)` | `Promise<{ messageId }>` | Send a group message |
210
+ | `sendGroupMessage(groupId, plaintext, memberUserIds, options?)` | `Promise<{ messageId }>` | Send an end-to-end encrypted group message (sender-side per-recipient fan-out). `options.attachments` for E2EE group attachments. |
211
+ | `sendCleartextGroupMessage(groupId, plaintext)` | `Promise<{ messageId }>` | Send a plaintext (non-encrypted) group message; server fans out to every other member. |
186
212
  | `onGroupMessage(callback)` | `() => void` | Listen for group messages |
187
213
 
188
214
  ### 1-to-1 Calls
@@ -111,6 +111,31 @@ export declare class MessagingClient implements DropOnAirClient {
111
111
  deleteMessage(originalMessageId: string, toUserId: string, scope: 'FOR_EVERYONE' | 'FOR_ME'): Promise<{
112
112
  deleteId: string;
113
113
  }>;
114
+ /**
115
+ * Register this device's push notification token with the DropOnAir platform.
116
+ * The token is opaque to the server; the platform fans out push notifications
117
+ * via the customer's APNs / FCM / VAPID credentials when a sender attaches a
118
+ * pushPayload to a message and the recipient is offline.
119
+ *
120
+ * For iOS apps using VoIP push (PushKit), pass {@code voipToken} alongside the
121
+ * regular APNs token. The platform will route CALL_INVITE pushes via the VoIP
122
+ * token and message pushes via the regular token.
123
+ *
124
+ * Idempotent: calling twice with the same (platform, deviceId) upserts the
125
+ * stored token and refreshes its lastSeenAt timestamp.
126
+ */
127
+ registerPushToken(opts: {
128
+ platform: 'APNS' | 'FCM' | 'WEB_PUSH';
129
+ token: string;
130
+ voipToken?: string;
131
+ }): Promise<void>;
132
+ /**
133
+ * Unregister this device's push notification token (e.g. on logout). Future
134
+ * push fan-out for this (appId, userId, deviceId, platform) tuple is dropped.
135
+ */
136
+ unregisterPushToken(opts: {
137
+ platform: 'APNS' | 'FCM' | 'WEB_PUSH';
138
+ }): Promise<void>;
114
139
  sendCleartextMessage(toUserId: string, plaintext: string): Promise<{
115
140
  messageId: string;
116
141
  }>;
@@ -127,7 +152,32 @@ export declare class MessagingClient implements DropOnAirClient {
127
152
  addGroupMembers(groupId: string, userIds: string[]): Promise<GroupInfo>;
128
153
  removeGroupMember(groupId: string, userId: string): Promise<GroupInfo>;
129
154
  deleteGroup(groupId: string): Promise<void>;
130
- sendGroupMessage(groupId: string, plaintext: string): Promise<{
155
+ /**
156
+ * Send an end-to-end encrypted group message. Mirrors the Android and iOS
157
+ * SDKs: the SDK encrypts the plaintext per recipient device (sender-side
158
+ * fan-out), producing one `GroupMemberPayload` per member that the server
159
+ * then routes to that member's devices without ever seeing the plaintext.
160
+ *
161
+ * `memberUserIds` should be the current group membership minus the sender;
162
+ * fetch it from your backend / `getGroup()` before calling.
163
+ *
164
+ * For unencrypted group messaging (bots, public channels), use
165
+ * {@link sendCleartextGroupMessage} instead.
166
+ */
167
+ sendGroupMessage(groupId: string, plaintext: string, memberUserIds: string[], options?: {
168
+ attachments?: AttachmentRef[];
169
+ }): Promise<{
170
+ messageId: string;
171
+ }>;
172
+ /**
173
+ * Send a plaintext (non-encrypted) group message. The server fans out the
174
+ * cleartext to every other member of the group; useful for bots and public
175
+ * channels where E2EE isn't required.
176
+ *
177
+ * For E2EE group messaging, use {@link sendGroupMessage} which encrypts
178
+ * per-recipient before sending.
179
+ */
180
+ sendCleartextGroupMessage(groupId: string, plaintext: string): Promise<{
131
181
  messageId: string;
132
182
  }>;
133
183
  onGroupMessage(callback: GroupMessageCallback): () => void;
@@ -526,6 +526,57 @@ class MessagingClient {
526
526
  return { deleteId };
527
527
  }
528
528
  // ---------------------------------------------------------------------------
529
+ // Push notification token registration (PROTOCOL_VERSION 5+)
530
+ // ---------------------------------------------------------------------------
531
+ /**
532
+ * Register this device's push notification token with the DropOnAir platform.
533
+ * The token is opaque to the server; the platform fans out push notifications
534
+ * via the customer's APNs / FCM / VAPID credentials when a sender attaches a
535
+ * pushPayload to a message and the recipient is offline.
536
+ *
537
+ * For iOS apps using VoIP push (PushKit), pass {@code voipToken} alongside the
538
+ * regular APNs token. The platform will route CALL_INVITE pushes via the VoIP
539
+ * token and message pushes via the regular token.
540
+ *
541
+ * Idempotent: calling twice with the same (platform, deviceId) upserts the
542
+ * stored token and refreshes its lastSeenAt timestamp.
543
+ */
544
+ async registerPushToken(opts) {
545
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
546
+ throw new Error('DropOnAir websocket is not connected');
547
+ }
548
+ if (!opts.token || opts.token.trim().length === 0) {
549
+ throw new Error('Push token must not be empty');
550
+ }
551
+ const deviceId = this.deviceId ?? await this.getOrCreateDeviceId();
552
+ const frame = {
553
+ type: 'PUSH_REGISTER',
554
+ platform: opts.platform,
555
+ token: opts.token,
556
+ voipToken: opts.voipToken ?? '',
557
+ deviceId,
558
+ };
559
+ this.ws.send(this.codec.encodePushRegistrationFrame(frame));
560
+ }
561
+ /**
562
+ * Unregister this device's push notification token (e.g. on logout). Future
563
+ * push fan-out for this (appId, userId, deviceId, platform) tuple is dropped.
564
+ */
565
+ async unregisterPushToken(opts) {
566
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
567
+ throw new Error('DropOnAir websocket is not connected');
568
+ }
569
+ const deviceId = this.deviceId ?? await this.getOrCreateDeviceId();
570
+ const frame = {
571
+ type: 'PUSH_UNREGISTER',
572
+ platform: opts.platform,
573
+ token: '',
574
+ voipToken: '',
575
+ deviceId,
576
+ };
577
+ this.ws.send(this.codec.encodePushRegistrationFrame(frame));
578
+ }
579
+ // ---------------------------------------------------------------------------
529
580
  // Cleartext messaging (no E2EE key exchange required)
530
581
  // ---------------------------------------------------------------------------
531
582
  async sendCleartextMessage(toUserId, plaintext) {
@@ -679,7 +730,109 @@ class MessagingClient {
679
730
  // ---------------------------------------------------------------------------
680
731
  // Group messaging
681
732
  // ---------------------------------------------------------------------------
682
- async sendGroupMessage(groupId, plaintext) {
733
+ /**
734
+ * Send an end-to-end encrypted group message. Mirrors the Android and iOS
735
+ * SDKs: the SDK encrypts the plaintext per recipient device (sender-side
736
+ * fan-out), producing one `GroupMemberPayload` per member that the server
737
+ * then routes to that member's devices without ever seeing the plaintext.
738
+ *
739
+ * `memberUserIds` should be the current group membership minus the sender;
740
+ * fetch it from your backend / `getGroup()` before calling.
741
+ *
742
+ * For unencrypted group messaging (bots, public channels), use
743
+ * {@link sendCleartextGroupMessage} instead.
744
+ */
745
+ async sendGroupMessage(groupId, plaintext, memberUserIds, options) {
746
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
747
+ throw new Error('DropOnAir websocket is not connected');
748
+ }
749
+ if (!this.currentUserId) {
750
+ throw new Error('Missing sender identity from DropOnAir JWT subject');
751
+ }
752
+ if (this.rateLimited) {
753
+ throw new Error('Sending is temporarily blocked by LIMIT_REACHED');
754
+ }
755
+ if (this.dropOnAirJwt && (0, bytes_1.isJwtExpired)(this.dropOnAirJwt)) {
756
+ throw new Error('JWT_EXPIRED: Secure messaging session is reconnecting; please retry in a moment');
757
+ }
758
+ if (!memberUserIds || memberUserIds.length === 0) {
759
+ throw new Error('sendGroupMessage requires at least one recipient userId in memberUserIds');
760
+ }
761
+ const myDeviceId = this.deviceId ?? await this.getOrCreateDeviceId();
762
+ const messageId = crypto.randomUUID();
763
+ const clientMessageId = messageId;
764
+ const timestamp = Date.now();
765
+ const myIdentity = await this.cryptoService.getOrCreateIdentity();
766
+ const myPublicKeyBytes = (0, bytes_1.fromBase64)(myIdentity.publicKey);
767
+ // Per-member sender-side fan-out: one GroupMemberPayload per member, each
768
+ // containing one DeviceEncryptedPayload per device the member has registered.
769
+ const memberPayloads = [];
770
+ for (const memberId of memberUserIds) {
771
+ if (memberId === this.currentUserId)
772
+ continue; // skip self
773
+ const deviceKeys = await this.fetchDeviceKeys(memberId);
774
+ const devicePayloads = [];
775
+ if (deviceKeys.length > 0) {
776
+ for (const dk of deviceKeys) {
777
+ const sharedKey = await this.cryptoService.deriveSharedSecret(memberId, dk.publicKey, this.currentUserId);
778
+ const encrypted = await this.cryptoService.encrypt(plaintext, sharedKey, {
779
+ messageId,
780
+ senderId: this.currentUserId,
781
+ recipientId: memberId,
782
+ timestamp,
783
+ });
784
+ devicePayloads.push({
785
+ deviceId: dk.deviceId,
786
+ encryptedPayload: encrypted,
787
+ senderPublicKey: myPublicKeyBytes,
788
+ });
789
+ }
790
+ }
791
+ else {
792
+ // Legacy fallback: peer reports no multi-device keys. Encrypt once
793
+ // against the single legacy public key under deviceId="default", which
794
+ // matches the Android SDK convention.
795
+ const sharedKey = await this.getPeerSharedKey(memberId);
796
+ const encrypted = await this.cryptoService.encrypt(plaintext, sharedKey, {
797
+ messageId,
798
+ senderId: this.currentUserId,
799
+ recipientId: memberId,
800
+ timestamp,
801
+ });
802
+ devicePayloads.push({
803
+ deviceId: 'default',
804
+ encryptedPayload: encrypted,
805
+ senderPublicKey: myPublicKeyBytes,
806
+ });
807
+ }
808
+ memberPayloads.push({ userId: memberId, devicePayloads });
809
+ }
810
+ const frame = {
811
+ messageId,
812
+ appId: this.options.appId,
813
+ groupId,
814
+ fromUserId: this.currentUserId,
815
+ timestamp,
816
+ clientMessageId,
817
+ senderDeviceId: myDeviceId,
818
+ encryptionType: 0, // E2EE
819
+ memberPayloads,
820
+ };
821
+ if (options?.attachments && options.attachments.length > 0) {
822
+ frame.attachments = options.attachments.map(a => this.attachmentClient.toWire(a));
823
+ }
824
+ this.ws.send(this.codec.encodeGroupEnvelope(frame));
825
+ return { messageId };
826
+ }
827
+ /**
828
+ * Send a plaintext (non-encrypted) group message. The server fans out the
829
+ * cleartext to every other member of the group; useful for bots and public
830
+ * channels where E2EE isn't required.
831
+ *
832
+ * For E2EE group messaging, use {@link sendGroupMessage} which encrypts
833
+ * per-recipient before sending.
834
+ */
835
+ async sendCleartextGroupMessage(groupId, plaintext) {
683
836
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
684
837
  throw new Error('DropOnAir websocket is not connected');
685
838
  }
@@ -695,17 +848,17 @@ class MessagingClient {
695
848
  const myDeviceId = this.deviceId ?? await this.getOrCreateDeviceId();
696
849
  const messageId = crypto.randomUUID();
697
850
  const timestamp = Date.now();
698
- // For now, group messages use cleartext (E2EE group fanout requires
699
- // fetching device keys for every member, which is a future enhancement).
700
851
  const frame = {
701
852
  messageId,
853
+ appId: this.options.appId,
702
854
  groupId,
703
855
  fromUserId: this.currentUserId,
856
+ timestamp,
857
+ clientMessageId: messageId,
704
858
  senderDeviceId: myDeviceId,
705
- memberPayloads: [],
706
859
  encryptionType: 1, // CLEARTEXT
707
860
  plaintextPayload: plaintext,
708
- timestamp,
861
+ memberPayloads: [],
709
862
  };
710
863
  this.ws.send(this.codec.encodeGroupEnvelope(frame));
711
864
  return { messageId };
@@ -238,8 +238,23 @@ export interface DropOnAirClient {
238
238
  removeGroupMember(groupId: string, userId: string): Promise<GroupInfo>;
239
239
  /** Delete a group (requires OWNER role). */
240
240
  deleteGroup(groupId: string): Promise<void>;
241
- /** Send a cleartext message to a group. */
242
- sendGroupMessage(groupId: string, plaintext: string): Promise<{
241
+ /**
242
+ * Send an end-to-end encrypted group message. The SDK encrypts the
243
+ * plaintext per recipient device (sender-side fan-out) before sending.
244
+ * `memberUserIds` should be the current group membership minus the sender;
245
+ * pass `options.attachments` for per-recipient encrypted attachment refs.
246
+ */
247
+ sendGroupMessage(groupId: string, plaintext: string, memberUserIds: string[], options?: {
248
+ attachments?: import('../attachment/attachment-types').AttachmentRef[];
249
+ }): Promise<{
250
+ messageId: string;
251
+ }>;
252
+ /**
253
+ * Send a plaintext (non-encrypted) group message. The server fans out the
254
+ * cleartext to every other member. Use this for bots and public channels;
255
+ * use {@link sendGroupMessage} for E2EE group messaging.
256
+ */
257
+ sendCleartextGroupMessage(groupId: string, plaintext: string): Promise<{
243
258
  messageId: string;
244
259
  }>;
245
260
  /** Register a listener for incoming group messages. Returns an unsubscribe function. */
@@ -82,23 +82,27 @@ export interface WireGroupMemberPayload {
82
82
  }
83
83
  export interface WireGroupEnvelope {
84
84
  messageId: string;
85
+ appId?: string;
85
86
  groupId: string;
86
87
  fromUserId: string;
88
+ timestamp: number;
89
+ clientMessageId?: string;
87
90
  senderDeviceId: string;
88
- memberPayloads: WireGroupMemberPayload[];
89
91
  encryptionType: number;
90
92
  plaintextPayload?: string;
91
- timestamp: number;
93
+ memberPayloads: WireGroupMemberPayload[];
94
+ attachments?: WireAttachmentRef[];
92
95
  }
93
96
  export interface WireGroupMessageNotification {
94
97
  messageId: string;
95
98
  groupId: string;
96
99
  fromUserId: string;
97
- senderDeviceId: string;
98
- devicePayloads: WireDeviceEncryptedPayload[];
100
+ timestamp: number;
99
101
  encryptionType: number;
100
102
  plaintextPayload?: string;
101
- timestamp: number;
103
+ devicePayloads: WireDeviceEncryptedPayload[];
104
+ senderDeviceId: string;
105
+ attachments?: WireAttachmentRef[];
102
106
  }
103
107
  export interface WireGroupAck {
104
108
  messageId: string;
@@ -132,6 +136,18 @@ export interface WireMessageEditFrame {
132
136
  plaintextPayload?: string;
133
137
  clientEditId?: string;
134
138
  }
139
+ /**
140
+ * Push token registration / unregister. Client -> server only. The server
141
+ * acks with a regular Ack frame of type 'PUSH_REGISTERED' or
142
+ * 'PUSH_UNREGISTERED' and correlation 'PUSH:<deviceId>:<platform>'.
143
+ */
144
+ export interface WirePushRegistrationFrame {
145
+ type: 'PUSH_REGISTER' | 'PUSH_UNREGISTER';
146
+ platform: 'APNS' | 'FCM' | 'WEB_PUSH';
147
+ token: string;
148
+ voipToken?: string;
149
+ deviceId: string;
150
+ }
135
151
  /** Tombstone frame, scope FOR_EVERYONE or FOR_ME. */
136
152
  export interface WireMessageDeleteFrame {
137
153
  type: string;
@@ -185,6 +201,7 @@ export declare class ProtobufCodec {
185
201
  encodeGroupAck(value: WireGroupAck): Uint8Array;
186
202
  encodeMessageEditFrame(value: WireMessageEditFrame): Uint8Array;
187
203
  encodeMessageDeleteFrame(value: WireMessageDeleteFrame): Uint8Array;
204
+ encodePushRegistrationFrame(value: WirePushRegistrationFrame): Uint8Array;
188
205
  decodeFrame(payload: Uint8Array): InboundFrame;
189
206
  private tryDecodeEnvelope;
190
207
  private tryDecodeAck;
@@ -124,20 +124,22 @@ const GroupMemberPayloadType = new protobuf.Type('GroupMemberPayload')
124
124
  .add(new protobuf.Field('userId', 1, 'string'))
125
125
  .add(new protobuf.Field('devicePayloads', 2, 'DeviceEncryptedPayload', 'repeated'));
126
126
  const GroupEnvelopeEncryptionTypeEnum = new protobuf.Enum('EncryptionType', { E2EE: 0, CLEARTEXT: 1 });
127
- // NOTE: GroupEnvelope JS field numbers diverge from the proto contract (tracked
128
- // under phase 2.3 "JS encrypted group send parity"). Attachments support for
129
- // groups in JS will land with phase 2.3 / 2.4 once field numbers are aligned.
130
127
  const GroupEnvelopeType = new protobuf.Type('GroupEnvelope')
131
128
  .add(GroupEnvelopeEncryptionTypeEnum)
132
129
  .add(GroupMemberPayloadType)
130
+ .add(AttachmentRefType)
133
131
  .add(new protobuf.Field('messageId', 1, 'string'))
134
- .add(new protobuf.Field('groupId', 2, 'string'))
135
- .add(new protobuf.Field('fromUserId', 3, 'string'))
136
- .add(new protobuf.Field('senderDeviceId', 4, 'string'))
137
- .add(new protobuf.Field('memberPayloads', 5, 'GroupMemberPayload', 'repeated'))
138
- .add(new protobuf.Field('encryptionType', 6, 'EncryptionType'))
139
- .add(new protobuf.Field('plaintextPayload', 7, 'string'))
140
- .add(new protobuf.Field('timestamp', 8, 'int64'));
132
+ .add(new protobuf.Field('appId', 2, 'string'))
133
+ .add(new protobuf.Field('groupId', 3, 'string'))
134
+ .add(new protobuf.Field('fromUserId', 4, 'string'))
135
+ .add(new protobuf.Field('timestamp', 5, 'int64'))
136
+ .add(new protobuf.Field('clientMessageId', 6, 'string'))
137
+ .add(new protobuf.Field('senderDeviceId', 7, 'string'))
138
+ .add(new protobuf.Field('encryptionType', 8, 'EncryptionType'))
139
+ .add(new protobuf.Field('plaintextPayload', 9, 'string'))
140
+ .add(new protobuf.Field('memberPayloads', 10, 'GroupMemberPayload', 'repeated'))
141
+ // field 11 reserved upstream
142
+ .add(new protobuf.Field('attachments', 12, 'AttachmentRef', 'repeated'));
141
143
  const GroupNotifEncryptionTypeEnum = new protobuf.Enum('EncryptionType', { E2EE: 0, CLEARTEXT: 1 });
142
144
  const GroupNotifDeviceType = new protobuf.Type('DeviceEncryptedPayload')
143
145
  .add(new protobuf.Field('deviceId', 1, 'string'))
@@ -149,11 +151,12 @@ const GroupMessageNotificationType = new protobuf.Type('GroupMessageNotification
149
151
  .add(new protobuf.Field('messageId', 1, 'string'))
150
152
  .add(new protobuf.Field('groupId', 2, 'string'))
151
153
  .add(new protobuf.Field('fromUserId', 3, 'string'))
152
- .add(new protobuf.Field('senderDeviceId', 4, 'string'))
153
- .add(new protobuf.Field('devicePayloads', 5, 'DeviceEncryptedPayload', 'repeated'))
154
- .add(new protobuf.Field('encryptionType', 6, 'EncryptionType'))
155
- .add(new protobuf.Field('plaintextPayload', 7, 'string'))
156
- .add(new protobuf.Field('timestamp', 8, 'int64'));
154
+ .add(new protobuf.Field('timestamp', 4, 'int64'))
155
+ .add(new protobuf.Field('encryptionType', 5, 'EncryptionType'))
156
+ .add(new protobuf.Field('plaintextPayload', 6, 'string'))
157
+ .add(new protobuf.Field('devicePayloads', 7, 'DeviceEncryptedPayload', 'repeated'))
158
+ .add(new protobuf.Field('senderDeviceId', 8, 'string'))
159
+ .add(new protobuf.Field('attachments', 9, 'AttachmentRef', 'repeated'));
157
160
  const GroupAckType = new protobuf.Type('GroupAck')
158
161
  .add(new protobuf.Field('messageId', 1, 'string'))
159
162
  .add(new protobuf.Field('groupId', 2, 'string'))
@@ -205,6 +208,15 @@ const MessageDeleteFrameType = new protobuf.Type('MessageDeleteFrame')
205
208
  .add(new protobuf.Field('timestamp', 7, 'int64'))
206
209
  .add(new protobuf.Field('scope', 8, 'string'))
207
210
  .add(new protobuf.Field('clientDeleteId', 9, 'string'));
211
+ // Push token registration frame (PROTOCOL_VERSION 5+). Client to server only;
212
+ // the server emits a PUSH_REGISTERED / PUSH_UNREGISTERED Ack back. The token
213
+ // is opaque to the server and stored against (appId, userId, deviceId, platform).
214
+ const PushRegistrationFrameType = new protobuf.Type('PushRegistrationFrame')
215
+ .add(new protobuf.Field('type', 1, 'string'))
216
+ .add(new protobuf.Field('platform', 2, 'string'))
217
+ .add(new protobuf.Field('token', 3, 'string'))
218
+ .add(new protobuf.Field('voipToken', 4, 'string'))
219
+ .add(new protobuf.Field('deviceId', 5, 'string'));
208
220
  class ProtobufCodec {
209
221
  encodeEnvelope(value) {
210
222
  return EnvelopeType.encode(value).finish();
@@ -233,6 +245,9 @@ class ProtobufCodec {
233
245
  encodeMessageDeleteFrame(value) {
234
246
  return MessageDeleteFrameType.encode(value).finish();
235
247
  }
248
+ encodePushRegistrationFrame(value) {
249
+ return PushRegistrationFrameType.encode(value).finish();
250
+ }
236
251
  decodeFrame(payload) {
237
252
  // Edit and delete frames must be probed BEFORE Envelope, the type
238
253
  // discriminator at field 1 ("MESSAGE_EDIT" / "MESSAGE_DELETE") would
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.7.0";
10
+ export declare const SDK_VERSION = "0.9.0";
11
11
  /**
12
12
  * Binary encrypted-payload format version.
13
13
  * Included as the first byte of every encrypted payload so receivers can
@@ -19,4 +19,4 @@ export declare const PAYLOAD_FORMAT_VERSION = 1;
19
19
  * Increment when adding required proto fields or changing frame semantics.
20
20
  * The server advertises its supported range via GET /api/info.
21
21
  */
22
- export declare const PROTOCOL_VERSION = 4;
22
+ export declare const PROTOCOL_VERSION = 5;
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.7.0';
13
+ exports.SDK_VERSION = '0.9.0';
14
14
  /**
15
15
  * Binary encrypted-payload format version.
16
16
  * Included as the first byte of every encrypted payload so receivers can
@@ -22,4 +22,4 @@ exports.PAYLOAD_FORMAT_VERSION = 1;
22
22
  * Increment when adding required proto fields or changing frame semantics.
23
23
  * The server advertises its supported range via GET /api/info.
24
24
  */
25
- exports.PROTOCOL_VERSION = 4;
25
+ exports.PROTOCOL_VERSION = 5;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@droponair/sdk-js",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "DropOnAir SDK for end-to-end encrypted messaging",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",