@droponair/sdk-js 0.9.0 → 0.10.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,29 @@ This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.10.0], 2026-05-20
10
+
11
+ ### Added
12
+
13
+ - **Device trust.** New `client.listMyDevices()` returns the current user's registered devices; `client.revokeMyDevice(deviceId)` revokes one of them. A revoked device's live session is closed immediately and all future connections from it are denied (revoke is permanent — re-registering yields a fresh deviceId).
14
+ - **`DEVICE_REVOKED` event.** When this device is revoked (from another of the user's devices, or by the app owner), the SDK stops its reconnect loop, closes the socket, and surfaces a `DEVICE_REVOKED` event through `onEvent` so the app can clear local key storage and show a re-register prompt.
15
+ - New exported `DeviceInfo` type.
16
+
17
+ ### Notes
18
+
19
+ - No wire-format change; PROTOCOL_VERSION stays at 5. The new surface is REST (`/v1/devices`) plus an additive event type.
20
+ - Any device that completes a connection is implicitly trusted. There is no explicit pairing/attestation step in this release.
21
+
22
+ ---
23
+
24
+ ## [0.9.1], 2026-05-19
25
+
26
+ ### Fixed
27
+
28
+ - **`registerPushToken` / `unregisterPushToken` now exposed on the public `DropOnAirClient` type.** In 0.9.0 the methods were implemented on the `MessagingClient` class but missing from the exported `DropOnAirClient` interface, so TypeScript consumers couldn't call them through the `initialize()` return value. No wire-format change; this is a pure type-surface fix.
29
+
30
+ ---
31
+
9
32
  ## [0.9.0], 2026-05-19
10
33
 
11
34
  ### Added
package/README.md CHANGED
@@ -98,6 +98,23 @@ const client = await initialize(options);
98
98
  | `ack(messageId)` | `Promise<void>` | Manually acknowledge a message |
99
99
  | `registerPushToken({ platform, token, voipToken? })` | `Promise<void>` | Register this device for push notifications. `platform` is `'APNS'`, `'FCM'`, or `'WEB_PUSH'`. |
100
100
  | `unregisterPushToken({ platform })` | `Promise<void>` | Unregister this device for push notifications (e.g. on logout). |
101
+ | `listMyDevices()` | `Promise<DeviceInfo[]>` | List the current user's registered devices. |
102
+ | `revokeMyDevice(deviceId)` | `Promise<DeviceInfo>` | Revoke one of the current user's devices. Permanent. |
103
+
104
+ ### Device trust
105
+
106
+ Available since SDK `0.10.0`. Any device that completes a connection is implicitly trusted. `listMyDevices()` powers a "Your devices" screen; `revokeMyDevice()` cuts a device off immediately. When the *current* device is revoked, the SDK stops reconnecting and emits a `DEVICE_REVOKED` event via `onEvent` — listen for it to clear local key storage and prompt re-registration.
107
+
108
+ ```typescript
109
+ const devices = await client.listMyDevices();
110
+ await client.revokeMyDevice('old-phone-device-id');
111
+
112
+ client.onEvent(e => {
113
+ if (e.type === 'DEVICE_REVOKED') {
114
+ // This device was revoked elsewhere. Clear local keys and show a re-register screen.
115
+ }
116
+ });
117
+ ```
101
118
 
102
119
  ### Push notifications
103
120
 
@@ -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, GroupCallEventCallback, GroupInfo, GroupMessageCallback, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials } from './types';
3
+ import { BroadcastCallback, CallEventCallback, DropOnAirClient, EventCallback, GroupCallEventCallback, GroupInfo, GroupMessageCallback, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo } 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;
@@ -136,6 +136,27 @@ export declare class MessagingClient implements DropOnAirClient {
136
136
  unregisterPushToken(opts: {
137
137
  platform: 'APNS' | 'FCM' | 'WEB_PUSH';
138
138
  }): Promise<void>;
139
+ /**
140
+ * List the current user's registered devices. Each device is implicitly
141
+ * trusted once it has completed a connection. Useful for a "Your devices"
142
+ * settings screen.
143
+ */
144
+ listMyDevices(): Promise<DeviceInfo[]>;
145
+ /**
146
+ * Revoke one of the current user's devices. The revoked device's live
147
+ * WebSocket session (if any) is closed immediately and all future
148
+ * connection attempts from that device are denied. Revoke is permanent;
149
+ * the user must re-register the device (which yields a fresh deviceId).
150
+ */
151
+ revokeMyDevice(deviceId: string): Promise<DeviceInfo>;
152
+ /**
153
+ * Handles an inbound DEVICE_REVOKED event: this device has been revoked
154
+ * (by the user from another device, or by the app owner). Stops the
155
+ * reconnect loop and closes the socket. The DEVICE_REVOKED event is also
156
+ * surfaced through onEvent so the app can clear local key storage and
157
+ * show a re-register prompt.
158
+ */
159
+ private handleDeviceRevoked;
139
160
  sendCleartextMessage(toUserId: string, plaintext: string): Promise<{
140
161
  messageId: string;
141
162
  }>;
@@ -577,6 +577,59 @@ class MessagingClient {
577
577
  this.ws.send(this.codec.encodePushRegistrationFrame(frame));
578
578
  }
579
579
  // ---------------------------------------------------------------------------
580
+ // Device trust (PROTOCOL_VERSION 5+ / Phase 2b)
581
+ // ---------------------------------------------------------------------------
582
+ /**
583
+ * List the current user's registered devices. Each device is implicitly
584
+ * trusted once it has completed a connection. Useful for a "Your devices"
585
+ * settings screen.
586
+ */
587
+ async listMyDevices() {
588
+ const jwt = await this.getValidDropOnAirJwt(false);
589
+ const resp = await this.fetchFn(`${this.httpUrl}/v1/devices`, {
590
+ headers: { Authorization: `Bearer ${jwt}` },
591
+ });
592
+ if (!resp.ok) {
593
+ throw new Error(`Failed to list devices: ${resp.status}`);
594
+ }
595
+ return (await resp.json());
596
+ }
597
+ /**
598
+ * Revoke one of the current user's devices. The revoked device's live
599
+ * WebSocket session (if any) is closed immediately and all future
600
+ * connection attempts from that device are denied. Revoke is permanent;
601
+ * the user must re-register the device (which yields a fresh deviceId).
602
+ */
603
+ async revokeMyDevice(deviceId) {
604
+ const jwt = await this.getValidDropOnAirJwt(false);
605
+ const resp = await this.fetchFn(`${this.httpUrl}/v1/devices/${encodeURIComponent(deviceId)}`, {
606
+ method: 'DELETE',
607
+ headers: { Authorization: `Bearer ${jwt}` },
608
+ });
609
+ if (!resp.ok) {
610
+ throw new Error(`Failed to revoke device: ${resp.status}`);
611
+ }
612
+ return (await resp.json());
613
+ }
614
+ /**
615
+ * Handles an inbound DEVICE_REVOKED event: this device has been revoked
616
+ * (by the user from another device, or by the app owner). Stops the
617
+ * reconnect loop and closes the socket. The DEVICE_REVOKED event is also
618
+ * surfaced through onEvent so the app can clear local key storage and
619
+ * show a re-register prompt.
620
+ */
621
+ handleDeviceRevoked() {
622
+ this.shouldReconnect = false;
623
+ this.rateLimited = false;
624
+ if (this.reconnectTimer) {
625
+ clearTimeout(this.reconnectTimer);
626
+ this.reconnectTimer = null;
627
+ }
628
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
629
+ this.ws.close(4001, 'DEVICE_REVOKED');
630
+ }
631
+ }
632
+ // ---------------------------------------------------------------------------
580
633
  // Cleartext messaging (no E2EE key exchange required)
581
634
  // ---------------------------------------------------------------------------
582
635
  async sendCleartextMessage(toUserId, plaintext) {
@@ -1272,6 +1325,10 @@ class MessagingClient {
1272
1325
  if (frame.data.type === 'LIMIT_REACHED') {
1273
1326
  this.rateLimited = true;
1274
1327
  }
1328
+ if (frame.data.type === 'DEVICE_REVOKED') {
1329
+ this.log('device_revoked_received', { metadata: frame.data.metadata });
1330
+ this.handleDeviceRevoked();
1331
+ }
1275
1332
  if (frame.data.type === 'ERROR' && frame.data.reason === 'JWT_EXPIRED') {
1276
1333
  this.dropOnAirJwt = null;
1277
1334
  this.currentUserId = null;
@@ -132,6 +132,19 @@ export interface InitializeOptions {
132
132
  */
133
133
  autoAckIncomingMessages?: boolean;
134
134
  }
135
+ /** A registered device as returned by {@link DropOnAirClient.listMyDevices}. */
136
+ export interface DeviceInfo {
137
+ deviceId: string;
138
+ sdkVersion?: string;
139
+ protocolVersion?: string;
140
+ platform?: string;
141
+ /** ISO-8601 timestamps. */
142
+ firstSeenAt?: string;
143
+ lastSeenAt?: string;
144
+ /** Non-null once revoked. */
145
+ revokedAt?: string;
146
+ revokedBy?: 'END_USER' | 'APP_OWNER' | 'SYSTEM';
147
+ }
135
148
  export interface DropOnAirClient {
136
149
  connect(): Promise<void>;
137
150
  disconnect(): void;
@@ -170,6 +183,31 @@ export interface DropOnAirClient {
170
183
  onMessageEdit(callback: MessageEditCallback): () => void;
171
184
  /** Register a listener for incoming message-delete notifications. */
172
185
  onMessageDelete(callback: MessageDeleteCallback): () => void;
186
+ /**
187
+ * Register this device's push notification token. The platform delivers a
188
+ * push via APNs / FCM / Web Push when a sender attaches a pushPayload to
189
+ * a message and the recipient has no live WebSocket session. The token is
190
+ * opaque to the server. For iOS PushKit/CallKit, pass {@code voipToken}
191
+ * alongside the regular APNs token so incoming call invites wake the app
192
+ * via the VoIP push channel.
193
+ */
194
+ registerPushToken(opts: {
195
+ platform: 'APNS' | 'FCM' | 'WEB_PUSH';
196
+ token: string;
197
+ voipToken?: string;
198
+ }): Promise<void>;
199
+ /** Unregister this device's push notification token (e.g. on logout). */
200
+ unregisterPushToken(opts: {
201
+ platform: 'APNS' | 'FCM' | 'WEB_PUSH';
202
+ }): Promise<void>;
203
+ /** List the current user's registered devices. */
204
+ listMyDevices(): Promise<DeviceInfo[]>;
205
+ /**
206
+ * Revoke one of the current user's devices. The revoked device's live
207
+ * session is closed and all future connections from it are denied.
208
+ * Revoke is permanent.
209
+ */
210
+ revokeMyDevice(deviceId: string): Promise<DeviceInfo>;
173
211
  /**
174
212
  * Convenience: encrypts (E2EE), uploads to the customer's bucket, finalizes,
175
213
  * and returns an AttachmentRef ready to pass into {@link sendMessage}.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { InitializeOptions, DropOnAirClient } from './core/types';
2
2
  export { SDK_VERSION, PROTOCOL_VERSION, PAYLOAD_FORMAT_VERSION } from './version';
3
3
  export declare function initialize(options: InitializeOptions): Promise<DropOnAirClient>;
4
- export type { InitializeOptions, DropOnAirClient, DropOnAirEvent, MessageCallback, EventCallback, DecryptedMessage, KeyStorageAdapter, CallEvent, CallEventType, CallEventCallback, TurnCredentials, BroadcastMessage, BroadcastCallback, GroupInfo, GroupMemberInfo, DecryptedGroupMessage, GroupMessageCallback, GroupCallEvent, GroupCallEventType, GroupCallEventCallback, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, } from './core/types';
4
+ export type { InitializeOptions, DropOnAirClient, DropOnAirEvent, MessageCallback, EventCallback, DecryptedMessage, KeyStorageAdapter, CallEvent, CallEventType, CallEventCallback, TurnCredentials, BroadcastMessage, BroadcastCallback, GroupInfo, GroupMemberInfo, DecryptedGroupMessage, GroupMessageCallback, GroupCallEvent, GroupCallEventType, GroupCallEventCallback, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, DeviceInfo, } from './core/types';
5
5
  export type { AttachmentEncryptionType, AttachmentConversationType, AttachmentRef, DeviceWrappedKey, CreateUploadSessionOptions, UploadSession, PrepareAttachmentOptions, DownloadedAttachment, } from './attachment/attachment-types';
6
6
  declare const _default: {
7
7
  initialize: typeof initialize;
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.9.0";
10
+ export declare const SDK_VERSION = "0.10.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.9.0';
13
+ exports.SDK_VERSION = '0.10.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.9.0",
3
+ "version": "0.10.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",