@droponair/sdk-js 0.34.2 → 0.36.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 +20 -0
- package/dist/attachment/attachment-client.d.ts +2 -2
- package/dist/attachment/attachment-client.js +99 -9
- package/dist/attachment/attachment-types.d.ts +15 -0
- package/dist/attachment/attachment-types.js +15 -0
- package/dist/core/messaging-client.d.ts +1 -1
- package/dist/core/messaging-client.js +2 -2
- package/dist/core/types.d.ts +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.36.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Downloads carry on where they stopped.** A download that breaks keeps what it
|
|
8
|
+
already had and asks the storage for the rest by range, retrying six times with a
|
|
9
|
+
growing wait. Nothing to change in an app: it happens inside `downloadAttachment`.
|
|
10
|
+
|
|
11
|
+
## 0.35.0
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Download progress.** `downloadAttachment(ref, { onProgress })` reports bytes as
|
|
16
|
+
they arrive.
|
|
17
|
+
- **Real upload progress in browsers.** `onUploadProgress` now reports as the bytes
|
|
18
|
+
go up where `XMLHttpRequest` exists; elsewhere it still reports once at the end.
|
|
19
|
+
- **`AttachmentUnavailableError`.** `downloadAttachment` throws it, with the HTTP
|
|
20
|
+
status, when the file is no longer there to fetch: deleted once everybody had it,
|
|
21
|
+
past its deadline, or revoked. A failure worth trying again stays a plain `Error`.
|
|
22
|
+
|
|
3
23
|
## 0.34.2
|
|
4
24
|
|
|
5
25
|
### 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,43 @@ 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
|
+
}
|
|
43
|
+
/** How many times a broken download is tried again, and how long between tries. */
|
|
44
|
+
const DOWNLOAD_ATTEMPTS = 6;
|
|
45
|
+
const DOWNLOAD_BACKOFF_MS = [2000, 4000, 8000, 15000, 30000];
|
|
13
46
|
async function sha256Hex(bytes) {
|
|
14
47
|
const digest = await crypto.subtle.digest('SHA-256', asBufferSource(bytes));
|
|
15
48
|
const hex = [];
|
|
@@ -174,7 +207,11 @@ class AttachmentClient {
|
|
|
174
207
|
});
|
|
175
208
|
if (!resp.ok) {
|
|
176
209
|
const text = await resp.text().catch(() => '');
|
|
177
|
-
|
|
210
|
+
const message = `Failed to get download URL (HTTP ${resp.status}): ${text}`;
|
|
211
|
+
if (resp.status === 404 || resp.status === 410) {
|
|
212
|
+
throw new attachment_types_1.AttachmentUnavailableError(attachmentId, resp.status, message);
|
|
213
|
+
}
|
|
214
|
+
throw new Error(message);
|
|
178
215
|
}
|
|
179
216
|
const body = await resp.json();
|
|
180
217
|
return {
|
|
@@ -190,15 +227,50 @@ class AttachmentClient {
|
|
|
190
227
|
* Downloads + decrypts an attachment. For E2EE, finds the wrappedKey for the
|
|
191
228
|
* current device, unwraps it, then decrypts the stored bytes.
|
|
192
229
|
*/
|
|
193
|
-
async downloadAttachment(ref) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
230
|
+
async downloadAttachment(ref, opts = {}) {
|
|
231
|
+
// What has already arrived is kept between attempts, and the rest asked for by
|
|
232
|
+
// range, so a download that breaks does not start the file again. A fresh link
|
|
233
|
+
// is asked for each attempt, since a link lasts minutes.
|
|
234
|
+
let had = new Uint8Array(0);
|
|
235
|
+
let info;
|
|
236
|
+
for (let attempt = 1;; attempt++) {
|
|
237
|
+
try {
|
|
238
|
+
info = await this.getDownloadUrl(ref.attachmentId);
|
|
239
|
+
const headers = { ...info.headers };
|
|
240
|
+
if (had.length > 0)
|
|
241
|
+
headers.Range = `bytes=${had.length}-`;
|
|
242
|
+
const resp = await this.deps.fetchFn(info.url, { method: info.method, headers });
|
|
243
|
+
if (resp.status === 416 && had.length > 0)
|
|
244
|
+
break;
|
|
245
|
+
if (!resp.ok) {
|
|
246
|
+
const text = await resp.text().catch(() => '');
|
|
247
|
+
const message = `Failed to download attachment bytes (HTTP ${resp.status}): ${text}`;
|
|
248
|
+
// The link was good and the bytes are not there: deleted from storage
|
|
249
|
+
// underneath a record that still existed.
|
|
250
|
+
if (resp.status === 404)
|
|
251
|
+
throw new attachment_types_1.AttachmentUnavailableError(ref.attachmentId, 404, message);
|
|
252
|
+
throw new Error(message);
|
|
253
|
+
}
|
|
254
|
+
const resuming = resp.status === 206 && had.length > 0;
|
|
255
|
+
const base = resuming ? had : new Uint8Array(0);
|
|
256
|
+
const rest = await readWithProgress(resp, Math.max(0, info.sizeBytes - base.length), opts.onProgress
|
|
257
|
+
? (received, total) => opts.onProgress(base.length + received, base.length + total)
|
|
258
|
+
: undefined);
|
|
259
|
+
const joined = new Uint8Array(base.length + rest.length);
|
|
260
|
+
joined.set(base, 0);
|
|
261
|
+
joined.set(rest, base.length);
|
|
262
|
+
had = joined;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
if (error instanceof attachment_types_1.AttachmentUnavailableError)
|
|
267
|
+
throw error;
|
|
268
|
+
if (attempt >= DOWNLOAD_ATTEMPTS)
|
|
269
|
+
throw error;
|
|
270
|
+
await new Promise((resolve) => setTimeout(resolve, DOWNLOAD_BACKOFF_MS[Math.min(attempt - 1, DOWNLOAD_BACKOFF_MS.length - 1)]));
|
|
271
|
+
}
|
|
199
272
|
}
|
|
200
|
-
|
|
201
|
-
let stored = new Uint8Array(arrayBuf);
|
|
273
|
+
let stored = had;
|
|
202
274
|
if (ref.encryptionType === 'CLEARTEXT') {
|
|
203
275
|
await this.confirmReceived(ref.attachmentId);
|
|
204
276
|
return { attachmentId: ref.attachmentId, mimeType: info.mimeType, sizeBytes: info.sizeBytes, sha256: info.sha256, bytes: stored };
|
|
@@ -281,6 +353,24 @@ class AttachmentClient {
|
|
|
281
353
|
}
|
|
282
354
|
// ─── internals ───
|
|
283
355
|
async uploadBytes(session, bytes, onProgress) {
|
|
356
|
+
// In a browser an XMLHttpRequest can say how far the body has got; fetch cannot.
|
|
357
|
+
if (onProgress && typeof XMLHttpRequest !== 'undefined') {
|
|
358
|
+
await new Promise((resolve, reject) => {
|
|
359
|
+
const xhr = new XMLHttpRequest();
|
|
360
|
+
xhr.open(session.uploadMethod || 'PUT', session.uploadUrl);
|
|
361
|
+
for (const [k, v] of Object.entries(session.uploadHeaders ?? {}))
|
|
362
|
+
xhr.setRequestHeader(k, v);
|
|
363
|
+
xhr.upload.onprogress = (e) => onProgress(e.loaded, e.lengthComputable ? e.total : bytes.length);
|
|
364
|
+
xhr.onload = () => (xhr.status >= 200 && xhr.status < 300)
|
|
365
|
+
? resolve()
|
|
366
|
+
: reject(new Error(`Storage upload failed (HTTP ${xhr.status}): ${String(xhr.responseText).slice(0, 400)}`));
|
|
367
|
+
xhr.onerror = () => reject(new Error('Storage upload failed: network error'));
|
|
368
|
+
onProgress(0, bytes.length);
|
|
369
|
+
xhr.send(asBufferSource(bytes));
|
|
370
|
+
});
|
|
371
|
+
onProgress(bytes.length, bytes.length);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
284
374
|
const resp = await this.deps.fetchFn(session.uploadUrl, {
|
|
285
375
|
method: session.uploadMethod || 'PUT',
|
|
286
376
|
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') {
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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.
|
|
10
|
+
export declare const SDK_VERSION = "0.36.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.
|
|
13
|
+
exports.SDK_VERSION = '0.36.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