@droponair/sdk-js 0.33.0 → 0.34.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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.34.1
4
+
5
+ ### Fixed
6
+
7
+ - **A file was deleted before the last recipient could download it.** With
8
+ `deleteWhenEveryoneHasIt`, the platform counted a recipient as having the file the
9
+ moment it issued their download link, so the final recipient got a link to a file
10
+ already gone. The SDK now confirms receipt only after the download and decrypt
11
+ succeed, and the platform deletes only on those confirmations. Nothing to change
12
+ in an app: it happens inside `downloadAttachment`.
13
+
14
+ ## 0.34.0
15
+
16
+ ### Added
17
+
18
+ - **Files in group messages.** `sendGroupMessage(groupId, text, memberUserIds, { attachments })`
19
+ now carries attachment references, and a received `DecryptedGroupMessage` carries
20
+ `attachments`. Each member is handed the pointer and only the file keys wrapped for
21
+ their own devices. A file sent while a member was away arrives with the message when
22
+ they catch up, which previously carried nothing.
23
+ - **A lifetime per file.** `retentionSeconds` on an upload session asks for one file to
24
+ live a set time. Never longer than the app's own retention.
25
+ - **`deleteWhenEveryoneHasIt`.** The platform deletes the stored bytes as soon as every
26
+ recipient has fetched them, and answers a later request with "no longer available"
27
+ rather than a broken link.
28
+
3
29
  ## 0.33.0
4
30
 
5
31
  ### Added
package/README.md CHANGED
@@ -346,6 +346,8 @@ client.onMessage(async (msg) => {
346
346
  | `prepareAttachmentAndUpload(bytes, options)` | `Promise<AttachmentRef>` | Encrypt (E2EE), upload, finalize, return ref |
347
347
  | `createUploadSession(options)` | `Promise<UploadSession>` | Low-level: presigned PUT URL only |
348
348
  | `finalizeAttachment(attachmentId, sha256)` | `Promise<void>` | Low-level: commit integrity hash |
349
+ | `retentionSeconds` (upload option) | | How long this one file should live. Never granted longer than your own retention. |
350
+ | `deleteWhenEveryoneHasIt` (upload option) | | Delete the stored bytes as soon as every recipient has fetched them. A later request is answered "no longer available". |
349
351
  | `downloadAttachment(ref)` | `Promise<DownloadedAttachment>` | Get presigned URL + download + decrypt |
350
352
  | `revokeAttachment(attachmentId \| ref)` | `Promise<void>` | Revoke an attachment you sent; recipients get `ATTACHMENT_REVOKED` |
351
353
  | `sendMessage(toUserId, text, { attachments })` | `Promise<{ messageId }>` | Send with attachments |
@@ -63,6 +63,8 @@ export declare class AttachmentClient {
63
63
  * current device, unwraps it, then decrypts the stored bytes.
64
64
  */
65
65
  downloadAttachment(ref: AttachmentRef): Promise<DownloadedAttachment>;
66
+ /** Best effort: a missed confirmation only means the copy waits for its own expiry. */
67
+ private confirmReceived;
66
68
  /** Convert public AttachmentRef into the wire-format type for proto encoding. */
67
69
  toWire(ref: AttachmentRef): WireAttachmentRef;
68
70
  /** Convert a wire AttachmentRef (from a received message) into the public type. */
@@ -46,6 +46,10 @@ class AttachmentClient {
46
46
  conversationId: opts.conversationId ?? '',
47
47
  recipientUserIds: opts.recipientUserIds ?? [],
48
48
  };
49
+ if (opts.retentionSeconds !== undefined)
50
+ body.retentionSeconds = opts.retentionSeconds;
51
+ if (opts.deleteWhenEveryoneHasIt)
52
+ body.deleteWhenEveryoneHasIt = true;
49
53
  const resp = await this.deps.fetchFn(`${this.deps.httpUrl}/v1/attachments/upload-session`, {
50
54
  method: 'POST',
51
55
  headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
@@ -194,6 +198,7 @@ class AttachmentClient {
194
198
  const arrayBuf = await resp.arrayBuffer();
195
199
  let stored = new Uint8Array(arrayBuf);
196
200
  if (ref.encryptionType === 'CLEARTEXT') {
201
+ await this.confirmReceived(ref.attachmentId);
197
202
  return { attachmentId: ref.attachmentId, mimeType: info.mimeType, sizeBytes: info.sizeBytes, sha256: info.sha256, bytes: stored };
198
203
  }
199
204
  const myDeviceId = await this.deps.getCurrentDeviceId();
@@ -210,6 +215,10 @@ class AttachmentClient {
210
215
  const ciphertext = stored.slice(12);
211
216
  const aesKey = await crypto.subtle.importKey('raw', asBufferSource(fileKey), { name: 'AES-GCM' }, false, ['decrypt']);
212
217
  const plain = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: asBufferSource(fileNonce), tagLength: 128 }, aesKey, asBufferSource(ciphertext));
218
+ // Only now, with the bytes downloaded and readable, does this device say it has
219
+ // the file. A download link is not a file, so the platform deletes the stored
220
+ // copy on this confirmation, never on the link.
221
+ await this.confirmReceived(ref.attachmentId);
213
222
  return {
214
223
  attachmentId: ref.attachmentId,
215
224
  mimeType: info.mimeType,
@@ -218,6 +227,20 @@ class AttachmentClient {
218
227
  bytes: new Uint8Array(plain),
219
228
  };
220
229
  }
230
+ /** Best effort: a missed confirmation only means the copy waits for its own expiry. */
231
+ async confirmReceived(attachmentId) {
232
+ try {
233
+ const jwt = await this.deps.getValidDropOnAirJwt();
234
+ await this.deps.fetchFn(`${this.deps.httpUrl}/v1/attachments/${encodeURIComponent(attachmentId)}/received`, {
235
+ method: 'POST',
236
+ headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
237
+ body: '{}',
238
+ });
239
+ }
240
+ catch {
241
+ // The download itself succeeded; nothing to surface.
242
+ }
243
+ }
221
244
  /** Convert public AttachmentRef into the wire-format type for proto encoding. */
222
245
  toWire(ref) {
223
246
  return {
@@ -44,6 +44,17 @@ export interface CreateUploadSessionOptions {
44
44
  conversationId?: string;
45
45
  /** Recipient user IDs (used by the server for download authorization). */
46
46
  recipientUserIds?: string[];
47
+ /**
48
+ * How long this one file should live, in seconds. Optional, and never granted
49
+ * longer than the app's own retention. An app that works out a window per file
50
+ * sets it here instead of living with one number for everything.
51
+ */
52
+ retentionSeconds?: number;
53
+ /**
54
+ * Delete the stored bytes as soon as every recipient has fetched them. The
55
+ * file then exists only on the devices it was sent to.
56
+ */
57
+ deleteWhenEveryoneHasIt?: boolean;
47
58
  }
48
59
  /** Result of reserving an upload session. */
49
60
  export interface UploadSession {
@@ -1863,6 +1863,7 @@ class MessagingClient {
1863
1863
  fromUserId: notif.fromUserId,
1864
1864
  timestamp: notif.timestamp,
1865
1865
  plaintext,
1866
+ attachments: (notif.attachments ?? []).map(a => attachment_client_1.AttachmentClient.fromWire(a)),
1866
1867
  };
1867
1868
  for (const listener of this.groupMessageListeners) {
1868
1869
  listener(msg);
@@ -2643,6 +2644,24 @@ class MessagingClient {
2643
2644
  encryptedPayload: (0, bytes_1.fromBase64)(dp.encryptedPayloadBase64),
2644
2645
  senderPublicKey: (0, bytes_1.fromBase64)(dp.senderPublicKeyBase64),
2645
2646
  })),
2647
+ // Files travel with a late message too. Without this, somebody who was
2648
+ // away came back to a message that mentioned nothing at all: the live
2649
+ // path carried the pointer and the catch-up did not.
2650
+ attachments: (offline.attachments ?? []).map(a => ({
2651
+ attachmentId: a.attachmentId,
2652
+ storageHint: a.storageHint ?? '',
2653
+ mimeType: a.mimeType ?? '',
2654
+ sizeBytes: a.sizeBytes ?? 0,
2655
+ sha256: a.sha256 ?? '',
2656
+ encryptionType: a.encryptionType === 'CLEARTEXT' ? 1 : 0,
2657
+ thumbnailAttachmentId: a.thumbnailAttachmentId ?? '',
2658
+ wrappedKeys: (a.wrappedKeys ?? []).map(k => ({
2659
+ deviceId: k.deviceId,
2660
+ wrappedKey: (0, bytes_1.fromBase64)(k.wrappedKeyBase64 ?? ''),
2661
+ senderPublicKey: (0, bytes_1.fromBase64)(k.senderPublicKeyBase64 ?? ''),
2662
+ nonce: (0, bytes_1.fromBase64)(k.nonceBase64 ?? ''),
2663
+ })),
2664
+ })),
2646
2665
  };
2647
2666
  // The same path a live message takes, so decryption, de-duplication and
2648
2667
  // the callback behave identically.
@@ -222,6 +222,12 @@ export interface DecryptedGroupMessage {
222
222
  fromUserId: string;
223
223
  timestamp: number;
224
224
  plaintext: string;
225
+ /**
226
+ * Files this message points at, if any. The bytes are fetched separately with
227
+ * `downloadAttachment`; what arrives here is the pointer and the file key
228
+ * wrapped for this device.
229
+ */
230
+ attachments?: import('../attachment/attachment-types').AttachmentRef[];
225
231
  }
226
232
  export type GroupMessageCallback = (message: DecryptedGroupMessage) => void;
227
233
  export type GroupCallEventType = 'GROUP_CALL_INVITE' | 'GROUP_CALL_RINGING' | 'GROUP_CALL_JOIN' | 'GROUP_CALL_LEAVE' | 'GROUP_CALL_END' | 'GROUP_CALL_ENDED' | 'GROUP_CALL_PARTICIPANT_JOINED' | 'GROUP_CALL_PARTICIPANT_LEFT' | 'GROUP_CALL_PARTICIPANT_REMOVED' | 'GROUP_CALL_ALREADY_ACTIVE' | 'GROUP_CALL_SDP_OFFER' | 'GROUP_CALL_SDP_ANSWER' | 'GROUP_CALL_ICE_CANDIDATE' | 'GROUP_CALL_SCREEN_SHARE_STARTED' | 'GROUP_CALL_SCREEN_SHARE_STOPPED' | 'GROUP_CALL_HOST_TRANSFER' | 'GROUP_CALL_COHOST_APPOINT' | 'GROUP_CALL_COHOST_REVOKE' | 'GROUP_CALL_ROLE_CHANGED' | 'GROUP_CALL_MUTE_PARTICIPANT' | 'GROUP_CALL_REMOVE_PARTICIPANT' | 'GROUP_CALL_WAITING_ROOM_REQUEST' | 'GROUP_CALL_WAITING_ROOM_JOINED' | 'GROUP_CALL_WAITING_ROOM_ADMIT' | 'GROUP_CALL_WAITING_ROOM_ADMITTED' | 'GROUP_CALL_WAITING_ROOM_REJECT' | 'GROUP_CALL_WAITING_ROOM_REJECTED' | 'GROUP_CALL_JOINED' | 'GROUP_CALL_WAITING_ROOM_PENDING' | 'GROUP_CALL_HOST_REQUIRED' | 'GROUP_CALL_ROOM_CLOSED' | 'GROUP_CALL_STAGE_HAND_RAISED' | 'GROUP_CALL_STAGE_HAND_LOWERED' | 'GROUP_CALL_STAGE_PROMOTE' | 'GROUP_CALL_STAGE_DEMOTE' | 'GROUP_CALL_STAGE_QUESTION' | 'GROUP_CALL_RECORDING_STARTED' | 'GROUP_CALL_RECORDING_STOPPED' | 'GROUP_CALL_RECORDING_AVAILABLE';
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.33.0";
10
+ export declare const SDK_VERSION = "0.34.1";
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.33.0';
13
+ exports.SDK_VERSION = '0.34.1';
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.33.0",
3
+ "version": "0.34.1",
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",