@mdbase-dev/connect 0.1.0-beta.31 → 0.1.0-beta.33
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/README.md +112 -34
- package/dist/advanced.d.ts +9 -0
- package/dist/advanced.js +6 -0
- package/dist/application-session.d.ts +39 -14
- package/dist/application-session.js +201 -70
- package/dist/async.js +11 -5
- package/dist/authority-proof.d.ts +3 -0
- package/dist/authority-proof.js +24 -0
- package/dist/browser/integrity.json +1 -1
- package/dist/browser/mdbase-connect.min.js +3 -3
- package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
- package/dist/browser.d.ts +3 -0
- package/dist/browser.js +5 -0
- package/dist/capabilities.js +2 -1
- package/dist/collection-client.d.ts +36 -33
- package/dist/collection-client.js +549 -104
- package/dist/connect-authorization-helpers.d.ts +3 -0
- package/dist/connect-authorization-helpers.js +13 -0
- package/dist/connect-options.d.ts +12 -4
- package/dist/connect.d.ts +12 -4
- package/dist/connect.js +102 -103
- package/dist/connection-file-transport.d.ts +2 -2
- package/dist/connection-file-transport.js +4 -4
- package/dist/connection-notifications.d.ts +10 -4
- package/dist/connection-notifications.js +41 -23
- package/dist/connection-transport.d.ts +19 -15
- package/dist/connection-transport.js +202 -202
- package/dist/connection-types.d.ts +5 -4
- package/dist/connection.d.ts +47 -46
- package/dist/connection.js +320 -162
- package/dist/crypto-entry.d.ts +7 -0
- package/dist/crypto-entry.js +4 -0
- package/dist/crypto.js +4 -4
- package/dist/external-store.d.ts +10 -0
- package/dist/external-store.js +10 -0
- package/dist/file-transfer-internals.d.ts +23 -0
- package/dist/file-transfer-internals.js +224 -0
- package/dist/files.d.ts +36 -14
- package/dist/files.js +92 -222
- package/dist/hosted-file-request.js +3 -3
- package/dist/index.d.ts +24 -19
- package/dist/index.js +10 -18
- package/dist/internal-types.d.ts +9 -1
- package/dist/local-file-transport.d.ts +2 -2
- package/dist/local-file-transport.js +13 -13
- package/dist/mdbase-connect.d.ts +6 -4
- package/dist/mdbase-connect.js +9 -8
- package/dist/notifications.d.ts +3 -2
- package/dist/operation-helpers.d.ts +3 -3
- package/dist/operation-helpers.js +17 -34
- package/dist/operation-types.d.ts +435 -45
- package/dist/outcomes.d.ts +15 -15
- package/dist/outcomes.js +13 -1
- package/dist/pending-mutation-store.d.ts +13 -0
- package/dist/pending-mutation-store.js +64 -0
- package/dist/request-budget.d.ts +27 -0
- package/dist/request-budget.js +95 -0
- package/dist/runtime-utils.d.ts +2 -0
- package/dist/runtime-utils.js +19 -0
- package/dist/session.d.ts +5 -4
- package/dist/session.js +6 -5
- package/dist/stored-token.d.ts +10 -0
- package/dist/stored-token.js +60 -0
- package/dist/token-response.d.ts +11 -0
- package/dist/token-response.js +77 -0
- package/dist/watch-policy.d.ts +8 -0
- package/dist/watch-policy.js +12 -0
- package/package.json +12 -2
package/dist/files.js
CHANGED
|
@@ -2,48 +2,55 @@ import { FILE_PROTOCOL_VERSION } from "@mdbase-dev/connect-protocol";
|
|
|
2
2
|
import { MdbaseConnectError, connectError } from "./errors.js";
|
|
3
3
|
import { IncrementalSha256 } from "./file-sha256.js";
|
|
4
4
|
import { BinaryPartReader } from "./file-stream-source.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const MAX_CONCURRENCY = 8;
|
|
8
|
-
const MAX_OBJECT_ATTEMPTS = 3;
|
|
9
|
-
const MAX_BUFFERED_DOWNLOAD_BYTES = 64 * 1024 * 1024;
|
|
10
|
-
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
11
|
-
const SHA256_DIGEST = /^sha256:[0-9a-f]{64}$/u;
|
|
5
|
+
import { MAX_BUFFERED_DOWNLOAD_BYTES, MAX_OBJECT_ATTEMPTS, SHA256_DIGEST, UUID, browserObjectHeaders, chunkLength, hashBlob, mapConcurrent, normalizeFileError, requirePreparedPart, requireTransferSession, requireTransferStatus, retryChunk, sourceBlob, throwIfAborted, uploadBodyBytes, uploadBodyLength, validConcurrency, validPageSize } from "./file-transfer-internals.js";
|
|
6
|
+
import { createRequestBudget, resolveConnectTimeouts, withCooperativeRequestBudget } from "./request-budget.js";
|
|
12
7
|
/** Ergonomic facade over resumable, authority-specific file delivery. */
|
|
13
8
|
export class MdbaseFileClient {
|
|
14
9
|
capability;
|
|
15
10
|
request;
|
|
16
11
|
framed;
|
|
17
12
|
hosted;
|
|
18
|
-
|
|
13
|
+
timeouts;
|
|
14
|
+
constructor(capability, request, framed, hosted, timeouts = resolveConnectTimeouts()) {
|
|
19
15
|
this.capability = capability;
|
|
20
16
|
this.request = request;
|
|
21
17
|
this.framed = framed;
|
|
22
18
|
this.hosted = hosted;
|
|
19
|
+
this.timeouts = timeouts;
|
|
23
20
|
}
|
|
24
21
|
async *list(options = {}) {
|
|
25
|
-
this.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
22
|
+
const budget = createRequestBudget(options, this.timeouts.requestMs);
|
|
23
|
+
const signal = budget.signal;
|
|
24
|
+
try {
|
|
25
|
+
this.requireAction("list");
|
|
26
|
+
let after;
|
|
27
|
+
do {
|
|
28
|
+
throwIfAborted(signal);
|
|
29
|
+
const query = new URLSearchParams({
|
|
30
|
+
protocol_version: String(FILE_PROTOCOL_VERSION),
|
|
31
|
+
...(options.folder ? { folder: options.folder } : {}),
|
|
32
|
+
...(after ? { after } : {}),
|
|
33
|
+
...(options.pageSize ? { limit: String(validPageSize(options.pageSize)) } : {})
|
|
34
|
+
});
|
|
35
|
+
const page = await this.request("GET", `?${query.toString()}`, undefined, signal);
|
|
36
|
+
if (page.protocol_version !== 1 || page.type !== "files_page" || !Array.isArray(page.files)) {
|
|
37
|
+
throw connectError("invalid_operation_response", "The authority returned an invalid file page.");
|
|
38
|
+
}
|
|
39
|
+
for (const file of page.files) {
|
|
40
|
+
throwIfAborted(signal);
|
|
41
|
+
yield clientFileDescriptor(file);
|
|
42
|
+
}
|
|
43
|
+
after = page.next;
|
|
44
|
+
} while (after);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
budget.dispose();
|
|
48
|
+
}
|
|
45
49
|
}
|
|
46
50
|
async upload(path, source, options = {}) {
|
|
51
|
+
return withCooperativeRequestBudget(options, this.timeouts.uploadMs, (budget) => this.uploadWithinBudget(path, source, { ...options, signal: budget.signal, timeoutMs: null }));
|
|
52
|
+
}
|
|
53
|
+
async uploadWithinBudget(path, source, options) {
|
|
47
54
|
this.requireAction(options.ifRevision ? "replace" : "add");
|
|
48
55
|
const blob = sourceBlob(source, options.mediaType);
|
|
49
56
|
const concurrency = validConcurrency(options.concurrency);
|
|
@@ -63,6 +70,13 @@ export class MdbaseFileClient {
|
|
|
63
70
|
* Reuse `transferId` with a newly opened source after an ambiguous failure.
|
|
64
71
|
*/
|
|
65
72
|
async uploadStream(path, source, options = {}) {
|
|
73
|
+
return withCooperativeRequestBudget(options, this.timeouts.uploadMs, (budget) => this.uploadStreamWithinBudget(path, source, {
|
|
74
|
+
...options,
|
|
75
|
+
signal: budget.signal,
|
|
76
|
+
timeoutMs: null
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
async uploadStreamWithinBudget(path, source, options) {
|
|
66
80
|
this.requireAction(options.ifRevision ? "replace" : "add");
|
|
67
81
|
if (!Number.isSafeInteger(source.size) || source.size < 0) {
|
|
68
82
|
throw connectError("invalid_request", "Streamed file size must be a non-negative safe integer.");
|
|
@@ -116,16 +130,21 @@ export class MdbaseFileClient {
|
|
|
116
130
|
const partCount = session.strategy.kind === "object_put"
|
|
117
131
|
? 1
|
|
118
132
|
: Math.ceil(size / partSize);
|
|
133
|
+
const status = await this.request("GET", `transfers/${encodeURIComponent(transferId)}`, undefined, options.signal);
|
|
134
|
+
requireTransferStatus(status, session);
|
|
135
|
+
if (status.state !== "open" && status.state !== "committed") {
|
|
136
|
+
throw connectError("invalid_operation_response", `The authority cannot resume a ${status.state} file transfer.`);
|
|
137
|
+
}
|
|
119
138
|
prepareSource?.(partSize);
|
|
120
|
-
if (!framed &&
|
|
121
|
-
const replay = await this.tryReplayUploadCommit(transferId, session.strategy.kind === "object_multipart" ?
|
|
139
|
+
if (!framed && status.received.length === partCount) {
|
|
140
|
+
const replay = await this.tryReplayUploadCommit(transferId, session.strategy.kind === "object_multipart" ? status.uploaded_parts : undefined, options.signal);
|
|
122
141
|
if (replay) {
|
|
123
142
|
committed = true;
|
|
124
|
-
return replay.file;
|
|
143
|
+
return clientFileDescriptor(replay.file);
|
|
125
144
|
}
|
|
126
145
|
}
|
|
127
|
-
const received = new Set(
|
|
128
|
-
const uploadedParts = new Map(
|
|
146
|
+
const received = new Set(status.received);
|
|
147
|
+
const uploadedParts = new Map(status.uploaded_parts.map((part) => [part.part_number - 1, part]));
|
|
129
148
|
let transferredBytes = [...received].reduce((total, index) => total + chunkLength(size, partSize, index), 0);
|
|
130
149
|
const parts = await mapConcurrent(partCount, concurrency, async (partIndex) => {
|
|
131
150
|
const offset = partIndex * partSize;
|
|
@@ -160,7 +179,7 @@ export class MdbaseFileClient {
|
|
|
160
179
|
throw connectError("invalid_operation_response", "The authority returned an invalid file receipt.");
|
|
161
180
|
}
|
|
162
181
|
committed = true;
|
|
163
|
-
return receipt.file;
|
|
182
|
+
return clientFileDescriptor(receipt.file);
|
|
164
183
|
}
|
|
165
184
|
catch (error) {
|
|
166
185
|
throw normalizeFileError(error);
|
|
@@ -188,10 +207,13 @@ export class MdbaseFileClient {
|
|
|
188
207
|
const copy = new Uint8Array(chunk.byteLength);
|
|
189
208
|
copy.set(chunk);
|
|
190
209
|
return copy.buffer;
|
|
191
|
-
}), { type: file.
|
|
210
|
+
}), { type: file.mediaType ?? "" });
|
|
192
211
|
}
|
|
193
212
|
/** Download and verify a file with network backpressure and bounded buffering. */
|
|
194
213
|
async downloadStream(file, options = {}) {
|
|
214
|
+
return withCooperativeRequestBudget(options, this.timeouts.requestMs, (budget) => this.openDownloadStream(file, options, budget.signal));
|
|
215
|
+
}
|
|
216
|
+
async openDownloadStream(file, options, startupSignal) {
|
|
195
217
|
this.requireAction("read");
|
|
196
218
|
validConcurrency(options.concurrency);
|
|
197
219
|
const transferId = crypto.randomUUID();
|
|
@@ -201,9 +223,9 @@ export class MdbaseFileClient {
|
|
|
201
223
|
protocol_version: FILE_PROTOCOL_VERSION,
|
|
202
224
|
type: "open_file_download",
|
|
203
225
|
transfer_id: transferId,
|
|
204
|
-
file_id: file.
|
|
226
|
+
file_id: file.fileId,
|
|
205
227
|
revision: file.revision
|
|
206
|
-
},
|
|
228
|
+
}, startupSignal);
|
|
207
229
|
requireTransferSession(session, transferId, "download");
|
|
208
230
|
if (session.total_size !== file.size
|
|
209
231
|
|| session.strategy.kind !== "object_ranges"
|
|
@@ -240,7 +262,7 @@ export class MdbaseFileClient {
|
|
|
240
262
|
await this.abort(transferId);
|
|
241
263
|
};
|
|
242
264
|
const verify = () => {
|
|
243
|
-
if (receivedBytes !== file.size || `sha256:${hash.digestHex()}` !== file.
|
|
265
|
+
if (receivedBytes !== file.size || `sha256:${hash.digestHex()}` !== file.contentDigest) {
|
|
244
266
|
throw connectError("invalid_operation_response", "Downloaded file bytes failed integrity verification.");
|
|
245
267
|
}
|
|
246
268
|
};
|
|
@@ -333,15 +355,18 @@ export class MdbaseFileClient {
|
|
|
333
355
|
return new Uint8Array(await (await this.download(file, options)).arrayBuffer());
|
|
334
356
|
}
|
|
335
357
|
async move(file, path, options = {}) {
|
|
358
|
+
return withCooperativeRequestBudget(options, this.timeouts.requestMs, (budget) => this.moveWithinBudget(file, path, { ...options, signal: budget.signal, timeoutMs: null }));
|
|
359
|
+
}
|
|
360
|
+
async moveWithinBudget(file, path, options) {
|
|
336
361
|
this.requireAction("move");
|
|
337
362
|
const mutationId = options.mutationId ?? crypto.randomUUID();
|
|
338
363
|
let receipt;
|
|
339
364
|
try {
|
|
340
|
-
receipt = await this.request("POST", `${encodeURIComponent(file.
|
|
365
|
+
receipt = await this.request("POST", `${encodeURIComponent(file.fileId)}/move`, {
|
|
341
366
|
protocol_version: FILE_PROTOCOL_VERSION,
|
|
342
367
|
type: "move_file",
|
|
343
368
|
mutation_id: mutationId,
|
|
344
|
-
file_id: file.
|
|
369
|
+
file_id: file.fileId,
|
|
345
370
|
if_revision: options.ifRevision ?? file.revision,
|
|
346
371
|
from_path: file.path,
|
|
347
372
|
path,
|
|
@@ -356,22 +381,25 @@ export class MdbaseFileClient {
|
|
|
356
381
|
if (receipt.protocol_version !== 1
|
|
357
382
|
|| receipt.type !== "file_moved"
|
|
358
383
|
|| receipt.mutation_id !== mutationId
|
|
359
|
-
|| receipt.file.file_id !== file.
|
|
384
|
+
|| receipt.file.file_id !== file.fileId
|
|
360
385
|
|| receipt.file.path !== path) {
|
|
361
386
|
throw connectError("invalid_operation_response", "The authority returned an invalid file move receipt.");
|
|
362
387
|
}
|
|
363
|
-
return receipt.file;
|
|
388
|
+
return clientFileDescriptor(receipt.file);
|
|
364
389
|
}
|
|
365
390
|
async delete(file, options = {}) {
|
|
391
|
+
return withCooperativeRequestBudget(options, this.timeouts.requestMs, (budget) => this.deleteWithinBudget(file, { ...options, signal: budget.signal, timeoutMs: null }));
|
|
392
|
+
}
|
|
393
|
+
async deleteWithinBudget(file, options) {
|
|
366
394
|
this.requireAction("delete");
|
|
367
395
|
const mutationId = options.mutationId ?? crypto.randomUUID();
|
|
368
396
|
let receipt;
|
|
369
397
|
try {
|
|
370
|
-
receipt = await this.request("POST", `${encodeURIComponent(file.
|
|
398
|
+
receipt = await this.request("POST", `${encodeURIComponent(file.fileId)}/delete`, {
|
|
371
399
|
protocol_version: FILE_PROTOCOL_VERSION,
|
|
372
400
|
type: "delete_file",
|
|
373
401
|
mutation_id: mutationId,
|
|
374
|
-
file_id: file.
|
|
402
|
+
file_id: file.fileId,
|
|
375
403
|
if_revision: options.ifRevision ?? file.revision,
|
|
376
404
|
path: file.path
|
|
377
405
|
}, options.signal);
|
|
@@ -382,11 +410,18 @@ export class MdbaseFileClient {
|
|
|
382
410
|
if (receipt.protocol_version !== 1
|
|
383
411
|
|| receipt.type !== "file_deleted"
|
|
384
412
|
|| receipt.mutation_id !== mutationId
|
|
385
|
-
|| receipt.file_id !== file.
|
|
413
|
+
|| receipt.file_id !== file.fileId
|
|
386
414
|
|| receipt.previous_path !== file.path) {
|
|
387
415
|
throw connectError("invalid_operation_response", "The authority returned an invalid file delete receipt.");
|
|
388
416
|
}
|
|
389
|
-
return
|
|
417
|
+
return {
|
|
418
|
+
protocolVersion: receipt.protocol_version,
|
|
419
|
+
type: receipt.type,
|
|
420
|
+
mutationId: receipt.mutation_id,
|
|
421
|
+
fileId: receipt.file_id,
|
|
422
|
+
previousPath: receipt.previous_path,
|
|
423
|
+
revision: receipt.revision
|
|
424
|
+
};
|
|
390
425
|
}
|
|
391
426
|
async uploadPart(transferId, partIndex, offset, body, contentLength, requireEtag, signal) {
|
|
392
427
|
for (let attempt = 1; attempt <= MAX_OBJECT_ATTEMPTS; attempt += 1) {
|
|
@@ -465,180 +500,15 @@ export class MdbaseFileClient {
|
|
|
465
500
|
await this.request("DELETE", `transfers/${encodeURIComponent(transferId)}`).catch(() => undefined);
|
|
466
501
|
}
|
|
467
502
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
480
|
-
function sourceBlob(source, mediaType) {
|
|
481
|
-
if (source instanceof Blob) {
|
|
482
|
-
return mediaType && mediaType !== source.type
|
|
483
|
-
? source.slice(0, source.size, mediaType)
|
|
484
|
-
: source;
|
|
485
|
-
}
|
|
486
|
-
if (source instanceof ArrayBuffer)
|
|
487
|
-
return new Blob([source], { type: mediaType });
|
|
488
|
-
const copy = new Uint8Array(source.byteLength);
|
|
489
|
-
copy.set(new Uint8Array(source.buffer, source.byteOffset, source.byteLength));
|
|
490
|
-
return new Blob([copy.buffer], { type: mediaType });
|
|
491
|
-
}
|
|
492
|
-
function uploadBodyLength(body) {
|
|
493
|
-
return body instanceof Blob ? body.size : body.byteLength;
|
|
494
|
-
}
|
|
495
|
-
async function uploadBodyBytes(body) {
|
|
496
|
-
return body instanceof Blob
|
|
497
|
-
? new Uint8Array(await body.arrayBuffer())
|
|
498
|
-
: body;
|
|
499
|
-
}
|
|
500
|
-
function validConcurrency(value = DEFAULT_CONCURRENCY) {
|
|
501
|
-
if (!Number.isInteger(value) || value < 1 || value > MAX_CONCURRENCY) {
|
|
502
|
-
throw connectError("invalid_request", `File concurrency must be between 1 and ${MAX_CONCURRENCY}.`);
|
|
503
|
-
}
|
|
504
|
-
return value;
|
|
505
|
-
}
|
|
506
|
-
function validPageSize(value) {
|
|
507
|
-
if (!Number.isInteger(value) || value < 1 || value > 1_000) {
|
|
508
|
-
throw connectError("invalid_request", "File page size must be between 1 and 1000.");
|
|
509
|
-
}
|
|
510
|
-
return value;
|
|
511
|
-
}
|
|
512
|
-
function requireTransferSession(session, transferId, direction) {
|
|
513
|
-
if (session.protocol_version !== 1
|
|
514
|
-
|| session.type !== "file_transfer"
|
|
515
|
-
|| session.transfer_id !== transferId
|
|
516
|
-
|| session.direction !== direction
|
|
517
|
-
|| !Number.isSafeInteger(session.total_size)
|
|
518
|
-
|| session.total_size < 0
|
|
519
|
-
|| !Array.isArray(session.received)
|
|
520
|
-
|| !validTransferStrategy(session)) {
|
|
521
|
-
throw connectError("invalid_operation_response", "The authority returned an invalid file transfer session.");
|
|
522
|
-
}
|
|
523
|
-
const partSize = session.strategy.kind === "framed_chunks"
|
|
524
|
-
? session.strategy.chunk_size
|
|
525
|
-
: session.strategy.kind === "object_put"
|
|
526
|
-
? Math.max(1, session.total_size)
|
|
527
|
-
: session.strategy.part_size;
|
|
528
|
-
const partCount = session.strategy.kind === "object_put"
|
|
529
|
-
? 1
|
|
530
|
-
: Math.ceil(session.total_size / partSize);
|
|
531
|
-
if (new Set(session.received).size !== session.received.length
|
|
532
|
-
|| session.received.some((index) => !Number.isSafeInteger(index) || index < 0 || index >= partCount)) {
|
|
533
|
-
throw connectError("invalid_operation_response", "The authority returned invalid transfer progress.");
|
|
534
|
-
}
|
|
535
|
-
const uploadedParts = session.uploaded_parts ?? [];
|
|
536
|
-
if (!Array.isArray(uploadedParts)
|
|
537
|
-
|| uploadedParts.some((part, index) => !Number.isSafeInteger(part?.part_number)
|
|
538
|
-
|| part.part_number < 1
|
|
539
|
-
|| part.part_number > partCount
|
|
540
|
-
|| typeof part.etag !== "string"
|
|
541
|
-
|| part.etag.length === 0
|
|
542
|
-
|| part.etag.length > 255
|
|
543
|
-
|| (index > 0 && uploadedParts[index - 1].part_number >= part.part_number))
|
|
544
|
-
|| (session.strategy.kind === "object_multipart"
|
|
545
|
-
? uploadedParts.length !== session.received.length
|
|
546
|
-
|| uploadedParts.some((part, index) => part.part_number - 1 !== session.received[index])
|
|
547
|
-
: uploadedParts.length !== 0)) {
|
|
548
|
-
throw connectError("invalid_operation_response", "The authority returned invalid uploaded part receipts.");
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
function validTransferStrategy(session) {
|
|
552
|
-
if (session.strategy.kind === "framed_chunks") {
|
|
553
|
-
return session.protection === "grant_aead_v1"
|
|
554
|
-
&& Number.isSafeInteger(session.strategy.chunk_size)
|
|
555
|
-
&& session.strategy.chunk_size > 0;
|
|
556
|
-
}
|
|
557
|
-
if (session.protection !== "transport_tls")
|
|
558
|
-
return false;
|
|
559
|
-
if (session.strategy.kind === "object_put")
|
|
560
|
-
return true;
|
|
561
|
-
return Number.isSafeInteger(session.strategy.part_size) && session.strategy.part_size > 0;
|
|
562
|
-
}
|
|
563
|
-
function chunkLength(totalSize, partSize, index) {
|
|
564
|
-
return Math.min(partSize, Math.max(0, totalSize - index * partSize));
|
|
565
|
-
}
|
|
566
|
-
async function retryChunk(work, signal) {
|
|
567
|
-
for (let attempt = 1; attempt <= MAX_OBJECT_ATTEMPTS; attempt += 1) {
|
|
568
|
-
throwIfAborted(signal);
|
|
569
|
-
try {
|
|
570
|
-
return await work();
|
|
571
|
-
}
|
|
572
|
-
catch (error) {
|
|
573
|
-
if (signal?.aborted || attempt === MAX_OBJECT_ATTEMPTS)
|
|
574
|
-
throw error;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
throw new Error("Unreachable file chunk retry state.");
|
|
578
|
-
}
|
|
579
|
-
function requirePreparedPart(part, transferId, partIndex, offset, contentLength, method) {
|
|
580
|
-
let url;
|
|
581
|
-
try {
|
|
582
|
-
url = new URL(part.url);
|
|
583
|
-
}
|
|
584
|
-
catch {
|
|
585
|
-
throw connectError("invalid_operation_response", "The authority returned an invalid object URL.");
|
|
586
|
-
}
|
|
587
|
-
if (part.protocol_version !== 1
|
|
588
|
-
|| part.type !== "file_part"
|
|
589
|
-
|| part.transfer_id !== transferId
|
|
590
|
-
|| part.part_index !== partIndex
|
|
591
|
-
|| part.offset !== offset
|
|
592
|
-
|| part.content_length !== contentLength
|
|
593
|
-
|| part.method.toUpperCase() !== method
|
|
594
|
-
|| url.protocol !== "https:"
|
|
595
|
-
&& !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]", "::1"].includes(url.hostname))
|
|
596
|
-
|| url.username
|
|
597
|
-
|| url.password) {
|
|
598
|
-
throw connectError("invalid_operation_response", "The authority returned an invalid prepared file part.");
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
function browserObjectHeaders(headers) {
|
|
602
|
-
const result = new Headers();
|
|
603
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
604
|
-
if (["host", "content-length"].includes(name.toLowerCase()))
|
|
605
|
-
continue;
|
|
606
|
-
result.set(name, value);
|
|
607
|
-
}
|
|
608
|
-
return result;
|
|
609
|
-
}
|
|
610
|
-
async function mapConcurrent(count, concurrency, work) {
|
|
611
|
-
const results = new Array(count);
|
|
612
|
-
let next = 0;
|
|
613
|
-
await Promise.all(Array.from({ length: Math.min(count, concurrency) }, async () => {
|
|
614
|
-
while (next < count) {
|
|
615
|
-
const index = next;
|
|
616
|
-
next += 1;
|
|
617
|
-
results[index] = await work(index);
|
|
618
|
-
}
|
|
619
|
-
}));
|
|
620
|
-
return results;
|
|
621
|
-
}
|
|
622
|
-
function throwIfAborted(signal) {
|
|
623
|
-
if (signal?.aborted) {
|
|
624
|
-
throw connectError("operation_cancelled", "The file transfer was cancelled.", {
|
|
625
|
-
operationOutcome: "not_sent",
|
|
626
|
-
cause: signal.reason
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
function normalizeFileError(error) {
|
|
631
|
-
if (error instanceof MdbaseConnectError)
|
|
632
|
-
return error;
|
|
633
|
-
if (typeof DOMException !== "undefined"
|
|
634
|
-
&& error instanceof DOMException
|
|
635
|
-
&& error.name === "AbortError") {
|
|
636
|
-
return connectError("operation_cancelled", "The file transfer was cancelled.", {
|
|
637
|
-
operationOutcome: "not_sent",
|
|
638
|
-
cause: error
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
return connectError("temporarily_unavailable", "The file transfer could not be completed.", {
|
|
642
|
-
cause: error
|
|
643
|
-
});
|
|
503
|
+
function clientFileDescriptor(file) {
|
|
504
|
+
return {
|
|
505
|
+
fileId: file.file_id,
|
|
506
|
+
path: file.path,
|
|
507
|
+
revision: file.revision,
|
|
508
|
+
contentDigest: file.content_digest,
|
|
509
|
+
size: file.size,
|
|
510
|
+
...(file.media_type ? { mediaType: file.media_type } : {}),
|
|
511
|
+
mediaClass: file.media_class,
|
|
512
|
+
modifiedAt: file.modified_at
|
|
513
|
+
};
|
|
644
514
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { connectError } from "./errors.js";
|
|
2
|
-
import { apiError } from "./runtime-utils.js";
|
|
2
|
+
import { apiError, decodeJsonResponse } from "./runtime-utils.js";
|
|
3
3
|
export async function sendHostedFileRequest(token, method, path, input, signal, proofHeaders) {
|
|
4
4
|
if (!token.authority) {
|
|
5
5
|
throw connectError("not_remote_authority", "This authorization has no remote authority endpoint.");
|
|
@@ -30,7 +30,7 @@ export async function performHostedFileRequest(token, method, path, input, signa
|
|
|
30
30
|
}
|
|
31
31
|
response = await sendHostedFileRequest(active, method, path, input, signal, proofHeaders);
|
|
32
32
|
}
|
|
33
|
-
const body = await response
|
|
33
|
+
const body = await decodeJsonResponse(response, "invalid_operation_response", "The hosted authority returned an invalid file response.");
|
|
34
34
|
if (!response.ok) {
|
|
35
35
|
throw apiError(body, "operation_failed", "Collection file request failed.", response.status);
|
|
36
36
|
}
|
|
@@ -47,7 +47,7 @@ export async function performHostedFilePartRequest(token, path, expectedLength,
|
|
|
47
47
|
response = await sendHostedFileRequest(active, "GET", path, undefined, signal, proofHeaders);
|
|
48
48
|
}
|
|
49
49
|
if (!response.ok) {
|
|
50
|
-
const body = await response
|
|
50
|
+
const body = await decodeJsonResponse(response, "invalid_operation_response", "The hosted authority returned an invalid file range error response.");
|
|
51
51
|
throw apiError(body, "operation_failed", "Collection file range request failed.", response.status);
|
|
52
52
|
}
|
|
53
53
|
const contentLength = response.headers.get("content-length");
|
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
|
-
export
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export
|
|
7
|
-
export
|
|
8
|
-
export
|
|
9
|
-
export
|
|
10
|
-
export
|
|
11
|
-
export
|
|
12
|
-
export
|
|
13
|
-
export
|
|
14
|
-
export
|
|
15
|
-
export
|
|
16
|
-
export
|
|
17
|
-
export
|
|
1
|
+
export type { MdbaseDeviceAuthorization } from "./authorization-types.js";
|
|
2
|
+
export { MdbaseApplicationSession, MdbaseMemoryVerificationStore } from "./application-session.js";
|
|
3
|
+
export type { MdbaseApplicationSessionOptions, MdbaseApplicationVerificationStore, MdbaseDefinitionUpdate, MdbaseCollectionSetupUpdate, MdbaseApplicationSessionSnapshot } from "./application-session.js";
|
|
4
|
+
export type { MdbaseConnectOptions, MdbaseConnectTimeouts, MdbaseFrontmatter } from "./connect-options.js";
|
|
5
|
+
export { MdbaseConnect } from "./mdbase-connect.js";
|
|
6
|
+
export { MdbaseConnection } from "./connection.js";
|
|
7
|
+
export type { MdbaseAuthorizationTarget, MdbaseAuthorizeOptions, MdbaseConnectionAuthorizeOptions, MdbaseConnectEnvironment, MdbaseAuthorizationResult, MdbaseAuthorizationOutcome } from "./connection.js";
|
|
8
|
+
export type { MdbaseConnectionRoute, DirectAccessStatus, MdbaseConnectionInfo, MdbaseAuthorizationCapabilities, MdbaseSyncTransport, MdbaseSyncConnection } from "./connection-types.js";
|
|
9
|
+
export { effectiveCapabilities } from "./capabilities.js";
|
|
10
|
+
export type { MdbaseCapabilityState, MdbaseCapabilityEvidence, MdbaseEffectiveCapability, MdbaseEffectiveCapabilities } from "./capabilities.js";
|
|
11
|
+
export { MdbaseConnectError, isRetryableConnectError } from "./errors.js";
|
|
12
|
+
export type { ConnectErrorContext } from "./errors.js";
|
|
13
|
+
export { MdbaseFileClient } from "./files.js";
|
|
14
|
+
export type { MdbaseFileSource, CollectionFileDescriptor, MdbaseFileDeleteReceipt, MdbaseFileStreamSource, MdbaseFileProgress, MdbaseFileListOptions, MdbaseFileUploadOptions, MdbaseFileStreamUploadOptions, MdbaseFileDownloadOptions, MdbaseFileMoveOptions, MdbaseFileDeleteOptions } from "./files.js";
|
|
15
|
+
export { parseMdbasePushPayload, parseMdbaseNativeNotificationData, showMdbasePushNotification } from "./notifications.js";
|
|
16
|
+
export type { MdbaseNotificationRegistrationOptions, MdbaseNotificationRegistration, MdbaseNativeNotificationRegistrationOptions, MdbaseNativeNotificationRegistration, MdbaseNativeNotificationData, MdbasePushPayload } from "./notifications.js";
|
|
17
|
+
export type { ConnectSuccess, ConnectFailure, ConnectOutcome, CommonOperationProblemCode, RegistrationProblemCode, AuthorizationProblemCode, NotificationProblemCode, DirectAccessProblemCode, SessionProblemCode, CollectionSetupProblemCode, CollectionDescriptionProblemCode, CollectionReadProblemCode, CollectionQueryProblemCode, CollectionMutationProblemCode, CollectionChangesProblemCode, CollectionTypeProblemCode } from "./outcomes.js";
|
|
18
|
+
export type { MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, ReadInput, DataContractSelector, CollectionFileMetadata, DataContractViewIdentity, QueryRecord, RecordDocument, QueryInput, QueryProjection, QuerySelectionExpression, QueryOrder, QuerySummary, QueryResult, QueryPagesOptions, QueryAllOptions, QueryPage, ConnectRequestOptions, MutationEstimate, MutationProgressState, MutationProgress, MutationProgressOptions, RenameProgressOptions, DeleteProgressOptions, PendingMutationSummary, PendingMutation, CreateInput, UpdateInput, DeleteInput, DeleteResult, DeletePreflightResult, RenameInput, RenameResult, RenamePreflightResult, ReadTypeInput, CreateTypeInput, UpdateTypeInput, ReadViewSourceInput, CreateViewSourceInput, UpdateViewSourceInput, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, SavedViewPresentation, SavedViewSource, SavedViewProperty, SavedNamedView, SavedViewDocument, SavedViewList, SavedViewSourceDocument, SavedViewExecution, ContractSetupChoice, TypePackResourceDiff, TypePackReceipt, TypePackAssessment, TypePackApplyResult, ConfigurationContributionValue, ConfigurationRequirement, ConfigurationProvision, ApplicationCollectionSetupRequirements, ApplicationCollectionSetupProvisions, ConfigurationSetupConflict, ConfigurationSetupAssessment, CollectionSetupAssessment, CollectionSetupReceipt, CollectionSetupApplyResult, AssessTypePackInput, ApplyTypePackInput, AssessCollectionSetupInput, ApplyCollectionSetupInput, ChangesInput, WatchOptions, WatchRetryOptions, WatchStatus, WatchInput, MdbaseWatchSubscription, CollectionChange, CollectionChangesPage, CollectionDescription, CollectionContractDescriptor, CollectionContractImplementationDescriptor, CollectionTypeDescriptor } from "./operation-types.js";
|
|
19
|
+
export { externalStore } from "./external-store.js";
|
|
20
|
+
export type { MdbaseExternalStore } from "./external-store.js";
|
|
21
|
+
export { MdbaseBrowserSelection, MdbaseMemorySelection } from "./selection.js";
|
|
22
|
+
export type { MdbaseSelectionHistory, MdbaseApplicationSelection, MdbaseBrowserSelectionOptions } from "./selection.js";
|
|
18
23
|
export type { MdbaseUnavailableReason } from "./session.js";
|
|
19
|
-
export {
|
|
20
|
-
export type { ApplicationProvisions, ApplicationCapabilityId, ApplicationCapabilityRequirements, ApplicationRequirements, ApplicationNotifications, ApplicationAuthorizationBinding, ApplicationAuthorizationProof,
|
|
24
|
+
export { MDBASE_RECORD_CREATED_CONTRACT, MDBASE_TIMER_FIRED_CONTRACT, operationsForApplicationCapabilities } from "@mdbase-dev/connect-protocol";
|
|
25
|
+
export type { ApplicationProvisions, ApplicationCapabilityId, ApplicationCapabilityRequirements, ApplicationRequirements, ApplicationNotifications, ApplicationAuthorizationBinding, ApplicationAuthorizationProof, FileAction, FileCapability, CollectionOperation as MdbaseOperation, CollectionTypeDocument, ConnectOperationOutcome, ConnectProblem, ConnectProblemCategory, ConnectProblemCode, ConnectProblemDetailsByCode, ConnectRecoveryAction, ContractRequirement, GrantScope, JsonObject, MdbaseAppManifest, NotificationCriterion, MdbaseDiagnostic, MdbaseOperationEnvelope, KnownConnectProblem, TypePackManifest, TypePackManifestResource, TypePackProvision, TypePackSourceResource, UnknownConnectProblem } from "@mdbase-dev/connect-protocol";
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export
|
|
7
|
-
export
|
|
8
|
-
export
|
|
9
|
-
export
|
|
10
|
-
export
|
|
11
|
-
export * from "./capabilities.js";
|
|
12
|
-
export * from "./errors.js";
|
|
13
|
-
export * from "./files.js";
|
|
14
|
-
export * from "./notifications.js";
|
|
15
|
-
export * from "./outcomes.js";
|
|
16
|
-
export * from "./operation-types.js";
|
|
17
|
-
export * from "./selection.js";
|
|
18
|
-
export { createPkce } from "./runtime-utils.js";
|
|
1
|
+
export { MdbaseApplicationSession, MdbaseMemoryVerificationStore } from "./application-session.js";
|
|
2
|
+
export { MdbaseConnect } from "./mdbase-connect.js";
|
|
3
|
+
export { MdbaseConnection } from "./connection.js";
|
|
4
|
+
export { effectiveCapabilities } from "./capabilities.js";
|
|
5
|
+
export { MdbaseConnectError, isRetryableConnectError } from "./errors.js";
|
|
6
|
+
export { MdbaseFileClient } from "./files.js";
|
|
7
|
+
export { parseMdbasePushPayload, parseMdbaseNativeNotificationData, showMdbasePushNotification } from "./notifications.js";
|
|
8
|
+
export { externalStore } from "./external-store.js";
|
|
9
|
+
export { MdbaseBrowserSelection, MdbaseMemorySelection } from "./selection.js";
|
|
10
|
+
export { MDBASE_RECORD_CREATED_CONTRACT, MDBASE_TIMER_FIRED_CONTRACT, operationsForApplicationCapabilities } from "@mdbase-dev/connect-protocol";
|
package/dist/internal-types.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { ApplicationNotifications, ApplicationRequirements, CollectionOperation, EncryptedRelayOperationRequest, GrantEncryption, FileCapability, GrantScope } from "@mdbase-dev/connect-protocol";
|
|
1
|
+
import type { ApplicationNotifications, ApplicationRequirements, CollectionOperation, EncryptedRelayOperationRequest, GrantEncryption, FileCapability, GrantScope, MdbaseOperationRequest, MutationOperationIdentifier } from "@mdbase-dev/connect-protocol";
|
|
2
2
|
import { encryptRelayRequest } from "./crypto.js";
|
|
3
3
|
export interface Application {
|
|
4
4
|
id: string;
|
|
5
|
+
family_identity: string;
|
|
5
6
|
manifest_digest: string;
|
|
6
7
|
name: string;
|
|
7
8
|
distribution?: "web" | "portable";
|
|
@@ -58,10 +59,16 @@ export interface PendingMutation {
|
|
|
58
59
|
collectionId: string;
|
|
59
60
|
grantId?: string;
|
|
60
61
|
keyId?: string;
|
|
62
|
+
keyHandle?: string;
|
|
63
|
+
applicationId?: string;
|
|
64
|
+
encryption?: GrantEncryption;
|
|
61
65
|
operation: CollectionOperation;
|
|
66
|
+
mutation?: MutationOperationIdentifier;
|
|
62
67
|
inputFingerprint: string;
|
|
63
68
|
requestId: string;
|
|
64
69
|
envelope?: EncryptedRelayOperationRequest;
|
|
70
|
+
/** Exact plaintext protocol request for transports that do not use a grant envelope. */
|
|
71
|
+
request?: MdbaseOperationRequest;
|
|
65
72
|
createdAt: number;
|
|
66
73
|
}
|
|
67
74
|
export interface OperationAttempt {
|
|
@@ -70,6 +77,7 @@ export interface OperationAttempt {
|
|
|
70
77
|
encryptedRequest?: Awaited<ReturnType<typeof encryptRelayRequest>>;
|
|
71
78
|
directDeliveryUncertain?: boolean;
|
|
72
79
|
pendingMutation?: boolean;
|
|
80
|
+
pendingMutationRecord?: PendingMutation;
|
|
73
81
|
resumingMutation?: boolean;
|
|
74
82
|
}
|
|
75
83
|
export declare const DEFAULT_OPERATIONS: CollectionOperation[];
|
|
@@ -5,8 +5,8 @@ interface LocalFileTransportOptions {
|
|
|
5
5
|
keyStore: GrantKeyStore;
|
|
6
6
|
serverUrl: string;
|
|
7
7
|
loopbackUrl: string;
|
|
8
|
-
authorizedToken(): Promise<StoredToken | null>;
|
|
9
|
-
refreshAuthorization(): Promise<StoredToken>;
|
|
8
|
+
authorizedToken(signal?: AbortSignal): Promise<StoredToken | null>;
|
|
9
|
+
refreshAuthorization(signal?: AbortSignal): Promise<StoredToken>;
|
|
10
10
|
shouldAttemptDirect(token: StoredToken): Promise<boolean>;
|
|
11
11
|
onDirectAvailable(): void;
|
|
12
12
|
onDirectUnavailable(): void;
|