@droponair/sdk-js 0.34.2 → 0.35.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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.35.0
4
+
5
+ ### Added
6
+
7
+ - **Download progress.** `downloadAttachment(ref, { onProgress })` reports bytes as
8
+ they arrive.
9
+ - **Real upload progress in browsers.** `onUploadProgress` now reports as the bytes
10
+ go up where `XMLHttpRequest` exists; elsewhere it still reports once at the end.
11
+ - **`AttachmentUnavailableError`.** `downloadAttachment` throws it, with the HTTP
12
+ status, when the file is no longer there to fetch: deleted once everybody had it,
13
+ past its deadline, or revoked. A failure worth trying again stays a plain `Error`.
14
+
3
15
  ## 0.34.2
4
16
 
5
17
  ### Fixed
@@ -4,7 +4,7 @@ interface DeviceKeyInfo {
4
4
  deviceId: string;
5
5
  publicKey: string;
6
6
  }
7
- import { AttachmentRef, CreateUploadSessionOptions, DownloadedAttachment, PrepareAttachmentOptions, UploadSession } from './attachment-types';
7
+ import { AttachmentRef, CreateUploadSessionOptions, DownloadAttachmentOptions, DownloadedAttachment, PrepareAttachmentOptions, UploadSession } from './attachment-types';
8
8
  interface AttachmentClientDeps {
9
9
  httpUrl: string;
10
10
  fetchFn: typeof fetch;
@@ -62,7 +62,7 @@ export declare class AttachmentClient {
62
62
  * Downloads + decrypts an attachment. For E2EE, finds the wrappedKey for the
63
63
  * current device, unwraps it, then decrypts the stored bytes.
64
64
  */
65
- downloadAttachment(ref: AttachmentRef): Promise<DownloadedAttachment>;
65
+ downloadAttachment(ref: AttachmentRef, opts?: DownloadAttachmentOptions): Promise<DownloadedAttachment>;
66
66
  /** Best effort: a missed confirmation only means the copy waits for its own expiry. */
67
67
  private confirmReceived;
68
68
  /** Convert public AttachmentRef into the wire-format type for proto encoding. */
@@ -6,10 +6,40 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.AttachmentClient = void 0;
7
7
  const tweetnacl_1 = __importDefault(require("tweetnacl"));
8
8
  const bytes_1 = require("../core/bytes");
9
+ const attachment_types_1 = require("./attachment-types");
9
10
  const STORAGE_PRIVATE = 'droponair.identity.privateKey.v1';
10
11
  function asBufferSource(data) {
11
12
  return new Uint8Array(data);
12
13
  }
14
+ /** Reads a response body, saying how far it has got when asked to. */
15
+ async function readWithProgress(resp, expectedBytes, onProgress) {
16
+ const reader = onProgress && resp.body ? resp.body.getReader() : null;
17
+ if (!reader || !onProgress) {
18
+ const bytes = new Uint8Array(await resp.arrayBuffer());
19
+ onProgress?.(bytes.length, bytes.length);
20
+ return bytes;
21
+ }
22
+ const header = Number(resp.headers?.get?.('content-length'));
23
+ const total = Number.isFinite(header) && header > 0 ? header : expectedBytes;
24
+ const chunks = [];
25
+ let received = 0;
26
+ onProgress(0, total);
27
+ for (;;) {
28
+ const { done, value } = await reader.read();
29
+ if (done)
30
+ break;
31
+ chunks.push(value);
32
+ received += value.length;
33
+ onProgress(received, total);
34
+ }
35
+ const out = new Uint8Array(received);
36
+ let offset = 0;
37
+ for (const chunk of chunks) {
38
+ out.set(chunk, offset);
39
+ offset += chunk.length;
40
+ }
41
+ return out;
42
+ }
13
43
  async function sha256Hex(bytes) {
14
44
  const digest = await crypto.subtle.digest('SHA-256', asBufferSource(bytes));
15
45
  const hex = [];
@@ -174,7 +204,11 @@ class AttachmentClient {
174
204
  });
175
205
  if (!resp.ok) {
176
206
  const text = await resp.text().catch(() => '');
177
- throw new Error(`Failed to get download URL (HTTP ${resp.status}): ${text}`);
207
+ const message = `Failed to get download URL (HTTP ${resp.status}): ${text}`;
208
+ if (resp.status === 404 || resp.status === 410) {
209
+ throw new attachment_types_1.AttachmentUnavailableError(attachmentId, resp.status, message);
210
+ }
211
+ throw new Error(message);
178
212
  }
179
213
  const body = await resp.json();
180
214
  return {
@@ -190,15 +224,19 @@ class AttachmentClient {
190
224
  * Downloads + decrypts an attachment. For E2EE, finds the wrappedKey for the
191
225
  * current device, unwraps it, then decrypts the stored bytes.
192
226
  */
193
- async downloadAttachment(ref) {
227
+ async downloadAttachment(ref, opts = {}) {
194
228
  const info = await this.getDownloadUrl(ref.attachmentId);
195
229
  const resp = await this.deps.fetchFn(info.url, { method: info.method, headers: info.headers });
196
230
  if (!resp.ok) {
197
231
  const text = await resp.text().catch(() => '');
198
- throw new Error(`Failed to download attachment bytes (HTTP ${resp.status}): ${text}`);
232
+ const message = `Failed to download attachment bytes (HTTP ${resp.status}): ${text}`;
233
+ // The link was good and the bytes are not there: deleted from storage
234
+ // underneath a record that still existed.
235
+ if (resp.status === 404)
236
+ throw new attachment_types_1.AttachmentUnavailableError(ref.attachmentId, 404, message);
237
+ throw new Error(message);
199
238
  }
200
- const arrayBuf = await resp.arrayBuffer();
201
- let stored = new Uint8Array(arrayBuf);
239
+ let stored = await readWithProgress(resp, info.sizeBytes, opts.onProgress);
202
240
  if (ref.encryptionType === 'CLEARTEXT') {
203
241
  await this.confirmReceived(ref.attachmentId);
204
242
  return { attachmentId: ref.attachmentId, mimeType: info.mimeType, sizeBytes: info.sizeBytes, sha256: info.sha256, bytes: stored };
@@ -281,6 +319,24 @@ class AttachmentClient {
281
319
  }
282
320
  // ─── internals ───
283
321
  async uploadBytes(session, bytes, onProgress) {
322
+ // In a browser an XMLHttpRequest can say how far the body has got; fetch cannot.
323
+ if (onProgress && typeof XMLHttpRequest !== 'undefined') {
324
+ await new Promise((resolve, reject) => {
325
+ const xhr = new XMLHttpRequest();
326
+ xhr.open(session.uploadMethod || 'PUT', session.uploadUrl);
327
+ for (const [k, v] of Object.entries(session.uploadHeaders ?? {}))
328
+ xhr.setRequestHeader(k, v);
329
+ xhr.upload.onprogress = (e) => onProgress(e.loaded, e.lengthComputable ? e.total : bytes.length);
330
+ xhr.onload = () => (xhr.status >= 200 && xhr.status < 300)
331
+ ? resolve()
332
+ : reject(new Error(`Storage upload failed (HTTP ${xhr.status}): ${String(xhr.responseText).slice(0, 400)}`));
333
+ xhr.onerror = () => reject(new Error('Storage upload failed: network error'));
334
+ onProgress(0, bytes.length);
335
+ xhr.send(asBufferSource(bytes));
336
+ });
337
+ onProgress(bytes.length, bytes.length);
338
+ return;
339
+ }
284
340
  const resp = await this.deps.fetchFn(session.uploadUrl, {
285
341
  method: session.uploadMethod || 'PUT',
286
342
  headers: session.uploadHeaders,
@@ -97,6 +97,21 @@ export interface PrepareAttachmentOptions {
97
97
  /** Mime type of the thumbnail bytes (defaults to 'image/jpeg'). */
98
98
  thumbnailMimeType?: string;
99
99
  }
100
+ /** Options for downloadAttachment. */
101
+ export interface DownloadAttachmentOptions {
102
+ /** Called as bytes arrive, with the bytes received so far and the total. */
103
+ onProgress?: (bytesReceived: number, totalBytes: number) => void;
104
+ }
105
+ /**
106
+ * The file is not there to fetch: deleted once everybody had it, past its
107
+ * deadline, revoked, or never existed. Nothing will bring it back, unlike a
108
+ * failure worth trying again. `status` is the HTTP status that said so.
109
+ */
110
+ export declare class AttachmentUnavailableError extends Error {
111
+ readonly attachmentId: string;
112
+ readonly status: number;
113
+ constructor(attachmentId: string, status: number, message: string);
114
+ }
100
115
  /** Bytes payload returned by downloadAttachment. */
101
116
  export interface DownloadedAttachment {
102
117
  attachmentId: string;
@@ -9,3 +9,18 @@
9
9
  // used for message payloads.
10
10
  // ---------------------------------------------------------------------------
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.AttachmentUnavailableError = void 0;
13
+ /**
14
+ * The file is not there to fetch: deleted once everybody had it, past its
15
+ * deadline, revoked, or never existed. Nothing will bring it back, unlike a
16
+ * failure worth trying again. `status` is the HTTP status that said so.
17
+ */
18
+ class AttachmentUnavailableError extends Error {
19
+ constructor(attachmentId, status, message) {
20
+ super(message);
21
+ this.attachmentId = attachmentId;
22
+ this.status = status;
23
+ this.name = 'AttachmentUnavailableError';
24
+ }
25
+ }
26
+ exports.AttachmentUnavailableError = AttachmentUnavailableError;
@@ -113,7 +113,7 @@ export declare class MessagingClient implements DropOnAirClient {
113
113
  prepareAttachmentAndUpload(bytes: Uint8Array, options: PrepareAttachmentOptions): Promise<AttachmentRef>;
114
114
  createUploadSession(options: CreateUploadSessionOptions): Promise<UploadSession>;
115
115
  finalizeAttachment(attachmentId: string, sha256: string): Promise<void>;
116
- downloadAttachment(ref: AttachmentRef): Promise<DownloadedAttachment>;
116
+ downloadAttachment(ref: AttachmentRef, options?: import('../attachment/attachment-types').DownloadAttachmentOptions): Promise<DownloadedAttachment>;
117
117
  revokeAttachment(attachment: string | AttachmentRef): Promise<void>;
118
118
  /**
119
119
  * How the identity private key is held. See {@link KeyCustody}.
@@ -338,8 +338,8 @@ class MessagingClient {
338
338
  async finalizeAttachment(attachmentId, sha256) {
339
339
  return this.attachmentClient.finalize(attachmentId, sha256);
340
340
  }
341
- async downloadAttachment(ref) {
342
- return this.attachmentClient.downloadAttachment(ref);
341
+ async downloadAttachment(ref, options) {
342
+ return this.attachmentClient.downloadAttachment(ref, options);
343
343
  }
344
344
  async revokeAttachment(attachment) {
345
345
  if (typeof attachment === 'string') {
@@ -600,7 +600,7 @@ export interface DropOnAirClient {
600
600
  /** Low-level: finalize an upload, committing the integrity hash. */
601
601
  finalizeAttachment(attachmentId: string, sha256: string): Promise<void>;
602
602
  /** Download (and for E2EE decrypt) an attachment referenced inside a received message. */
603
- downloadAttachment(ref: import('../attachment/attachment-types').AttachmentRef): Promise<import('../attachment/attachment-types').DownloadedAttachment>;
603
+ downloadAttachment(ref: import('../attachment/attachment-types').AttachmentRef, options?: import('../attachment/attachment-types').DownloadAttachmentOptions): Promise<import('../attachment/attachment-types').DownloadedAttachment>;
604
604
  /**
605
605
  * Revoke an attachment you sent. The platform stops issuing download URLs
606
606
  * for it and notifies recipients; bytes already downloaded cannot be
package/dist/index.d.ts CHANGED
@@ -23,7 +23,8 @@ export { SDK_VERSION, PROTOCOL_VERSION, PAYLOAD_FORMAT_VERSION } from './version
23
23
  export declare function createSecureIdentity(store?: IdentityRecordStore): Promise<WebCryptoIdentityProvider | null>;
24
24
  export declare function initialize(options: InitializeOptions): Promise<DropOnAirClient>;
25
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, GroupMessageEditEvent, GroupMessageEditCallback, GroupMessageDeleteEvent, GroupMessageDeleteCallback, DeviceInfo, ReadReceiptEvent, ReadReceiptCallback, TypingEvent, TypingCallback, NotificationClearEvent, NotificationClearCallback, DraftSyncEvent, DraftSyncCallback, } from './core/types';
26
- export type { AttachmentEncryptionType, AttachmentConversationType, AttachmentRef, DeviceWrappedKey, CreateUploadSessionOptions, UploadSession, PrepareAttachmentOptions, DownloadedAttachment, } from './attachment/attachment-types';
26
+ export type { AttachmentEncryptionType, AttachmentConversationType, AttachmentRef, DeviceWrappedKey, CreateUploadSessionOptions, UploadSession, PrepareAttachmentOptions, DownloadedAttachment, DownloadAttachmentOptions, } from './attachment/attachment-types';
27
+ export { AttachmentUnavailableError } from './attachment/attachment-types';
27
28
  export { SseTransport, type SseTransportOptions, type SseFrameHandler, type SseStateHandler, } from './transport/sse-transport';
28
29
  export { WebTransportTransport, type WebTransportTransportOptions, type WTFrameHandler, type WTStateHandler, } from './transport/webtransport-transport';
29
30
  export { selectTransport, type TransportLane, type SelectTransportOptions, } from './transport/auto-select';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MemoryIdentityRecordStore = exports.IndexedDbIdentityRecordStore = exports.WebCryptoIdentityProvider = exports.selectTransport = exports.WebTransportTransport = exports.SseTransport = exports.PAYLOAD_FORMAT_VERSION = exports.PROTOCOL_VERSION = exports.SDK_VERSION = void 0;
3
+ exports.MemoryIdentityRecordStore = exports.IndexedDbIdentityRecordStore = exports.WebCryptoIdentityProvider = exports.selectTransport = exports.WebTransportTransport = exports.SseTransport = exports.AttachmentUnavailableError = exports.PAYLOAD_FORMAT_VERSION = exports.PROTOCOL_VERSION = exports.SDK_VERSION = void 0;
4
4
  exports.createSecureIdentity = createSecureIdentity;
5
5
  exports.initialize = initialize;
6
6
  const messaging_client_1 = require("./core/messaging-client");
@@ -61,6 +61,8 @@ async function initialize(options) {
61
61
  }
62
62
  return client;
63
63
  }
64
+ var attachment_types_1 = require("./attachment/attachment-types");
65
+ Object.defineProperty(exports, "AttachmentUnavailableError", { enumerable: true, get: function () { return attachment_types_1.AttachmentUnavailableError; } });
64
66
  // HTTP fallback lane primitive for restrictive networks.
65
67
  var sse_transport_1 = require("./transport/sse-transport");
66
68
  Object.defineProperty(exports, "SseTransport", { enumerable: true, get: function () { return sse_transport_1.SseTransport; } });
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.34.2";
10
+ export declare const SDK_VERSION = "0.35.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.34.2';
13
+ exports.SDK_VERSION = '0.35.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.34.2",
3
+ "version": "0.35.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",