@telecrypt-io/storage 0.3.0 → 0.5.7
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 +60 -15
- package/dist/TeleCryptIOStorage.d.ts +88 -47
- package/dist/TeleCryptIOStorage.js +1420 -232
- package/dist/core/constants.d.ts +16 -0
- package/dist/core/constants.js +54 -0
- package/dist/core/errors.d.ts +67 -1
- package/dist/core/errors.js +109 -1
- package/dist/core/http.d.ts +24 -0
- package/dist/core/http.js +199 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/oidc.d.ts +47 -69
- package/dist/core/oidc.js +1196 -137
- package/dist/core/operations.d.ts +44 -35
- package/dist/core/operations.js +1296 -220
- package/dist/core/poll.d.ts +2 -1
- package/dist/core/poll.js +59 -3
- package/dist/core/types.d.ts +29 -3
- package/dist/core/validation.d.ts +3 -0
- package/dist/core/validation.js +10 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +11 -9
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Maximum plaintext media body accepted by the non-streaming SDK surface. */
|
|
2
|
+
export declare const MAX_MEDIA_FILE_BYTES: number;
|
|
3
|
+
/** Matrix's canonical identifier bounds used at authenticated-client boundaries. */
|
|
4
|
+
export declare const MAX_MATRIX_USER_ID_BYTES = 255;
|
|
5
|
+
export declare const MAX_MATRIX_DEVICE_ID_LENGTH = 128;
|
|
6
|
+
export declare const MAX_MATRIX_IDENTIFIER_LENGTH = 512;
|
|
7
|
+
/**
|
|
8
|
+
* Validates a new/current Matrix identity and binds its server name exactly to
|
|
9
|
+
* the caller's independently trusted Matrix topology. The Matrix server name
|
|
10
|
+
* is deliberately explicit: a deployment may expose its homeserver through a
|
|
11
|
+
* backend hostname that differs from the user-visible Matrix server name.
|
|
12
|
+
*/
|
|
13
|
+
export declare function validateCanonicalMatrixUserId(value: unknown, serverName: unknown): string;
|
|
14
|
+
export declare function validateMatrixDeviceId(value: unknown): string;
|
|
15
|
+
export declare function validateMatrixRoomId(value: unknown, name?: string): string;
|
|
16
|
+
export declare function validateMatrixEventId(value: unknown, name?: string): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Maximum plaintext media body accepted by the non-streaming SDK surface. */
|
|
2
|
+
export const MAX_MEDIA_FILE_BYTES = 128 * 1024 * 1024;
|
|
3
|
+
/** Matrix's canonical identifier bounds used at authenticated-client boundaries. */
|
|
4
|
+
export const MAX_MATRIX_USER_ID_BYTES = 255;
|
|
5
|
+
export const MAX_MATRIX_DEVICE_ID_LENGTH = 128;
|
|
6
|
+
export const MAX_MATRIX_IDENTIFIER_LENGTH = 512;
|
|
7
|
+
const MATRIX_SERVER_NAME_PATTERN = "(?:\\[[0-9A-Fa-f:.]+\\]|[A-Za-z0-9.-]+)(?::\\d{1,5})?";
|
|
8
|
+
const MATRIX_SERVER_NAME_REGEXP = new RegExp(`^${MATRIX_SERVER_NAME_PATTERN}$`);
|
|
9
|
+
const MATRIX_USER_ID_PATTERN = new RegExp(`^@([A-Za-z0-9._=+\\-/]+):(${MATRIX_SERVER_NAME_PATTERN})$`);
|
|
10
|
+
const MATRIX_DEVICE_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/;
|
|
11
|
+
const MATRIX_ROOM_ID_PATTERN = new RegExp(`^![A-Za-z0-9._~+\\/-]+:${MATRIX_SERVER_NAME_PATTERN}$`);
|
|
12
|
+
const MATRIX_EVENT_ID_PATTERN = /^\$[A-Za-z0-9._~:/+\-]{1,511}$/;
|
|
13
|
+
/**
|
|
14
|
+
* Validates a new/current Matrix identity and binds its server name exactly to
|
|
15
|
+
* the caller's independently trusted Matrix topology. The Matrix server name
|
|
16
|
+
* is deliberately explicit: a deployment may expose its homeserver through a
|
|
17
|
+
* backend hostname that differs from the user-visible Matrix server name.
|
|
18
|
+
*/
|
|
19
|
+
export function validateCanonicalMatrixUserId(value, serverName) {
|
|
20
|
+
if (typeof value !== "string" ||
|
|
21
|
+
new TextEncoder().encode(value).byteLength > MAX_MATRIX_USER_ID_BYTES) {
|
|
22
|
+
throw new Error("invalid Matrix user ID");
|
|
23
|
+
}
|
|
24
|
+
if (typeof serverName !== "string" || !MATRIX_SERVER_NAME_REGEXP.test(serverName)) {
|
|
25
|
+
throw new Error("invalid Matrix server name");
|
|
26
|
+
}
|
|
27
|
+
const match = value.match(MATRIX_USER_ID_PATTERN);
|
|
28
|
+
if (!match || match[2].toLowerCase() !== serverName.toLowerCase()) {
|
|
29
|
+
throw new Error("invalid Matrix user ID for this homeserver");
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
export function validateMatrixDeviceId(value) {
|
|
34
|
+
if (typeof value !== "string" || value.length > MAX_MATRIX_DEVICE_ID_LENGTH || !MATRIX_DEVICE_ID_PATTERN.test(value)) {
|
|
35
|
+
throw new Error("invalid Matrix device ID");
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
export function validateMatrixRoomId(value, name = "room ID") {
|
|
40
|
+
if (typeof value !== "string" ||
|
|
41
|
+
value.length > MAX_MATRIX_IDENTIFIER_LENGTH ||
|
|
42
|
+
!MATRIX_ROOM_ID_PATTERN.test(value)) {
|
|
43
|
+
throw new Error(`invalid Matrix ${name}`);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
export function validateMatrixEventId(value, name = "event ID") {
|
|
48
|
+
if (typeof value !== "string" ||
|
|
49
|
+
value.length > MAX_MATRIX_IDENTIFIER_LENGTH ||
|
|
50
|
+
!MATRIX_EVENT_ID_PATTERN.test(value)) {
|
|
51
|
+
throw new Error(`invalid Matrix ${name}`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -1,7 +1,73 @@
|
|
|
1
1
|
/** A user-facing operation error: a clean, prefix-free message meant to be
|
|
2
2
|
* shown directly to a user — never a raw stack trace. Thrown for expected failure
|
|
3
|
-
* conditions (bad login, wrong recovery key, missing file
|
|
3
|
+
* conditions (bad login, wrong recovery key, missing file, vault, or folder, not
|
|
4
4
|
* logged in, ...). Platform-agnostic: no Node/CLI dependencies. */
|
|
5
5
|
export declare class StorageError extends Error {
|
|
6
6
|
constructor(message: string);
|
|
7
7
|
}
|
|
8
|
+
/** Setup would replace account recovery state, which this SDK never does. */
|
|
9
|
+
export declare class RecoveryAlreadyConfiguredError extends StorageError {
|
|
10
|
+
readonly code: "RECOVERY_ALREADY_CONFIGURED";
|
|
11
|
+
constructor();
|
|
12
|
+
}
|
|
13
|
+
/** Restore failed without exposing provider or crypto-library error details. */
|
|
14
|
+
export declare class RecoveryRestoreError extends StorageError {
|
|
15
|
+
readonly code: "RECOVERY_RESTORE_FAILED";
|
|
16
|
+
constructor();
|
|
17
|
+
}
|
|
18
|
+
/** Setup failed before the generated Recovery Key could be safely returned. */
|
|
19
|
+
export declare class RecoverySetupError extends StorageError {
|
|
20
|
+
readonly code: "RECOVERY_SETUP_FAILED";
|
|
21
|
+
constructor();
|
|
22
|
+
}
|
|
23
|
+
/** The recovery setup transport result is ambiguous; state must be checked before retrying. */
|
|
24
|
+
export declare class RecoverySetupAmbiguousError extends StorageError {
|
|
25
|
+
readonly code: "RECOVERY_SETUP_AMBIGUOUS";
|
|
26
|
+
constructor();
|
|
27
|
+
}
|
|
28
|
+
/** Recovery restore was cancelled after the provider may have changed state. */
|
|
29
|
+
export declare class RecoveryRestoreAmbiguousError extends StorageError {
|
|
30
|
+
readonly code: "RECOVERY_RESTORE_AMBIGUOUS";
|
|
31
|
+
constructor();
|
|
32
|
+
}
|
|
33
|
+
/** A Matrix mutation was cancelled after its request started. */
|
|
34
|
+
export declare class MutationOutcomeUnknownError extends StorageError {
|
|
35
|
+
readonly code: "MUTATION_OUTCOME_UNKNOWN";
|
|
36
|
+
readonly operation: string;
|
|
37
|
+
constructor(operation?: string);
|
|
38
|
+
}
|
|
39
|
+
/** A multi-room mutation committed some work before a later step failed. */
|
|
40
|
+
export declare class MutationPartialError extends StorageError {
|
|
41
|
+
readonly code: "MUTATION_PARTIAL";
|
|
42
|
+
readonly operation: string;
|
|
43
|
+
readonly completedIds: readonly string[];
|
|
44
|
+
constructor(operation: string, completedIds: readonly string[], detail?: string);
|
|
45
|
+
}
|
|
46
|
+
/** A vault or folder still contains files or child folders. */
|
|
47
|
+
export declare class NonEmptyTreeError extends StorageError {
|
|
48
|
+
readonly code: "NON_EMPTY_TREE";
|
|
49
|
+
readonly treeId: string;
|
|
50
|
+
constructor(treeId: string);
|
|
51
|
+
}
|
|
52
|
+
/** Room creation timed out or returned an unusable identity. */
|
|
53
|
+
export declare class RoomCreationAmbiguousError extends StorageError {
|
|
54
|
+
readonly code: "ROOM_CREATION_AMBIGUOUS";
|
|
55
|
+
readonly operation: string;
|
|
56
|
+
constructor(operation?: string);
|
|
57
|
+
}
|
|
58
|
+
/** A newly-created room could not be fully rolled back. */
|
|
59
|
+
export declare class RoomCleanupIncompleteError extends StorageError {
|
|
60
|
+
readonly code: "ROOM_CLEANUP_INCOMPLETE";
|
|
61
|
+
readonly roomId: string;
|
|
62
|
+
constructor(roomId: string);
|
|
63
|
+
}
|
|
64
|
+
/** The SDK's single product file-size limit was exceeded. */
|
|
65
|
+
export declare class FileTooLargeError extends StorageError {
|
|
66
|
+
readonly code: "FILE_TOO_LARGE";
|
|
67
|
+
constructor();
|
|
68
|
+
}
|
|
69
|
+
/** The file event is present, but this device has no key to decrypt it. */
|
|
70
|
+
export declare class UndecryptableFileError extends StorageError {
|
|
71
|
+
readonly code: "FILE_UNDECRYPTABLE";
|
|
72
|
+
constructor();
|
|
73
|
+
}
|
package/dist/core/errors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** A user-facing operation error: a clean, prefix-free message meant to be
|
|
2
2
|
* shown directly to a user — never a raw stack trace. Thrown for expected failure
|
|
3
|
-
* conditions (bad login, wrong recovery key, missing file
|
|
3
|
+
* conditions (bad login, wrong recovery key, missing file, vault, or folder, not
|
|
4
4
|
* logged in, ...). Platform-agnostic: no Node/CLI dependencies. */
|
|
5
5
|
export class StorageError extends Error {
|
|
6
6
|
constructor(message) {
|
|
@@ -8,3 +8,111 @@ export class StorageError extends Error {
|
|
|
8
8
|
this.name = "StorageError";
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
/** Setup would replace account recovery state, which this SDK never does. */
|
|
12
|
+
export class RecoveryAlreadyConfiguredError extends StorageError {
|
|
13
|
+
code = "RECOVERY_ALREADY_CONFIGURED";
|
|
14
|
+
constructor() {
|
|
15
|
+
super("recovery is already configured for this account; use the existing Recovery Key or restore it on this device");
|
|
16
|
+
this.name = "RecoveryAlreadyConfiguredError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Restore failed without exposing provider or crypto-library error details. */
|
|
20
|
+
export class RecoveryRestoreError extends StorageError {
|
|
21
|
+
code = "RECOVERY_RESTORE_FAILED";
|
|
22
|
+
constructor() {
|
|
23
|
+
super("recovery restore failed; verify the Recovery Key and account");
|
|
24
|
+
this.name = "RecoveryRestoreError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Setup failed before the generated Recovery Key could be safely returned. */
|
|
28
|
+
export class RecoverySetupError extends StorageError {
|
|
29
|
+
code = "RECOVERY_SETUP_FAILED";
|
|
30
|
+
constructor() {
|
|
31
|
+
super("recovery setup did not complete; check recovery status before retrying");
|
|
32
|
+
this.name = "RecoverySetupError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** The recovery setup transport result is ambiguous; state must be checked before retrying. */
|
|
36
|
+
export class RecoverySetupAmbiguousError extends StorageError {
|
|
37
|
+
code = "RECOVERY_SETUP_AMBIGUOUS";
|
|
38
|
+
constructor() {
|
|
39
|
+
super("recovery setup may have completed; check recovery status before retrying");
|
|
40
|
+
this.name = "RecoverySetupAmbiguousError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Recovery restore was cancelled after the provider may have changed state. */
|
|
44
|
+
export class RecoveryRestoreAmbiguousError extends StorageError {
|
|
45
|
+
code = "RECOVERY_RESTORE_AMBIGUOUS";
|
|
46
|
+
constructor() {
|
|
47
|
+
super("recovery restore may have completed; check recovery status before retrying");
|
|
48
|
+
this.name = "RecoveryRestoreAmbiguousError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** A Matrix mutation was cancelled after its request started. */
|
|
52
|
+
export class MutationOutcomeUnknownError extends StorageError {
|
|
53
|
+
code = "MUTATION_OUTCOME_UNKNOWN";
|
|
54
|
+
operation;
|
|
55
|
+
constructor(operation = "operation") {
|
|
56
|
+
super(`${operation} outcome is unknown; reconcile current state before retrying`);
|
|
57
|
+
this.name = "MutationOutcomeUnknownError";
|
|
58
|
+
this.operation = operation;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** A multi-room mutation committed some work before a later step failed. */
|
|
62
|
+
export class MutationPartialError extends StorageError {
|
|
63
|
+
code = "MUTATION_PARTIAL";
|
|
64
|
+
operation;
|
|
65
|
+
completedIds;
|
|
66
|
+
constructor(operation, completedIds, detail) {
|
|
67
|
+
super(`${operation} failed after partial completion${detail ? `: ${detail}` : ""}; retry to reconcile current state`);
|
|
68
|
+
this.name = "MutationPartialError";
|
|
69
|
+
this.operation = operation;
|
|
70
|
+
this.completedIds = [...completedIds];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** A vault or folder still contains files or child folders. */
|
|
74
|
+
export class NonEmptyTreeError extends StorageError {
|
|
75
|
+
code = "NON_EMPTY_TREE";
|
|
76
|
+
treeId;
|
|
77
|
+
constructor(treeId) {
|
|
78
|
+
super("cannot delete a nonempty vault or folder; delete its files and empty child folders first");
|
|
79
|
+
this.name = "NonEmptyTreeError";
|
|
80
|
+
this.treeId = treeId;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Room creation timed out or returned an unusable identity. */
|
|
84
|
+
export class RoomCreationAmbiguousError extends StorageError {
|
|
85
|
+
code = "ROOM_CREATION_AMBIGUOUS";
|
|
86
|
+
operation;
|
|
87
|
+
constructor(operation = "room creation") {
|
|
88
|
+
super(`${operation} outcome is unknown; reconcile newly-created rooms before retrying`);
|
|
89
|
+
this.name = "RoomCreationAmbiguousError";
|
|
90
|
+
this.operation = operation;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** A newly-created room could not be fully rolled back. */
|
|
94
|
+
export class RoomCleanupIncompleteError extends StorageError {
|
|
95
|
+
code = "ROOM_CLEANUP_INCOMPLETE";
|
|
96
|
+
roomId;
|
|
97
|
+
constructor(roomId) {
|
|
98
|
+
super("created room cleanup is incomplete; retry cleanup for the created room");
|
|
99
|
+
this.name = "RoomCleanupIncompleteError";
|
|
100
|
+
this.roomId = roomId;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** The SDK's single product file-size limit was exceeded. */
|
|
104
|
+
export class FileTooLargeError extends StorageError {
|
|
105
|
+
code = "FILE_TOO_LARGE";
|
|
106
|
+
constructor() {
|
|
107
|
+
super("file exceeds the 128 MiB limit");
|
|
108
|
+
this.name = "FileTooLargeError";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** The file event is present, but this device has no key to decrypt it. */
|
|
112
|
+
export class UndecryptableFileError extends StorageError {
|
|
113
|
+
code = "FILE_UNDECRYPTABLE";
|
|
114
|
+
constructor() {
|
|
115
|
+
super("downloadFile: could not read file info from the event — it is likely undecryptable on this device (missing megolm session; try restoring from a Recovery Key)");
|
|
116
|
+
this.name = "UndecryptableFileError";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a response body without allowing an untrusted peer to control the
|
|
3
|
+
* amount of memory consumed. Callers decide whether a truncated body is an
|
|
4
|
+
* error and retain the response status when they need provider-error mapping.
|
|
5
|
+
*/
|
|
6
|
+
export interface BoundedResponseBody {
|
|
7
|
+
bytes: Uint8Array<ArrayBuffer>;
|
|
8
|
+
truncated: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface BoundedResponseOptions {
|
|
11
|
+
/** Error returned when the body signal aborts. */
|
|
12
|
+
abortError?: () => Error;
|
|
13
|
+
/** Keep a stream reader locked until its asynchronous cancellation settles. */
|
|
14
|
+
deferReaderRelease?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Reject a caller-facing operation as soon as a signal aborts. The underlying
|
|
18
|
+
* operation is deliberately not awaited after that point: browser/network
|
|
19
|
+
* implementations are allowed to ignore AbortSignal, but a bounded SDK API
|
|
20
|
+
* must still settle at its deadline. Its promise remains observed so a late
|
|
21
|
+
* rejection cannot become an unhandled rejection.
|
|
22
|
+
*/
|
|
23
|
+
export declare function raceWithAbort<T>(operation: PromiseLike<T>, signal: AbortSignal, onAbort: () => void, error: () => Error): Promise<T>;
|
|
24
|
+
export declare function readBoundedResponseBody(response: Response, maxBytes: number, signal?: AbortSignal, options?: BoundedResponseOptions): Promise<BoundedResponseBody>;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
function abortError() {
|
|
2
|
+
return new DOMException("The operation was aborted", "AbortError");
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Reject a caller-facing operation as soon as a signal aborts. The underlying
|
|
6
|
+
* operation is deliberately not awaited after that point: browser/network
|
|
7
|
+
* implementations are allowed to ignore AbortSignal, but a bounded SDK API
|
|
8
|
+
* must still settle at its deadline. Its promise remains observed so a late
|
|
9
|
+
* rejection cannot become an unhandled rejection.
|
|
10
|
+
*/
|
|
11
|
+
export function raceWithAbort(operation, signal, onAbort, error) {
|
|
12
|
+
if (signal.aborted) {
|
|
13
|
+
try {
|
|
14
|
+
onAbort();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Cleanup is best effort; the caller-facing abort error remains stable.
|
|
18
|
+
}
|
|
19
|
+
// The operation may already have been started by the caller. Observe its
|
|
20
|
+
// eventual rejection even though the public operation is already aborted.
|
|
21
|
+
void Promise.resolve(operation).catch(() => undefined);
|
|
22
|
+
return Promise.reject(error());
|
|
23
|
+
}
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
let settled = false;
|
|
26
|
+
const cleanup = () => signal.removeEventListener("abort", abort);
|
|
27
|
+
const finish = (callback) => {
|
|
28
|
+
if (settled)
|
|
29
|
+
return;
|
|
30
|
+
settled = true;
|
|
31
|
+
cleanup();
|
|
32
|
+
callback();
|
|
33
|
+
};
|
|
34
|
+
const abort = () => {
|
|
35
|
+
// A resolved fetch can still have its abort event delivered first when a
|
|
36
|
+
// caller aborts immediately after starting it. Let an already-resolved
|
|
37
|
+
// operation hand its response to the bounded body reader, which can then
|
|
38
|
+
// cancel that reader instead of losing it in the fetch race.
|
|
39
|
+
queueMicrotask(() => {
|
|
40
|
+
finish(() => {
|
|
41
|
+
try {
|
|
42
|
+
onAbort();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Cleanup is best effort; the caller-facing abort error remains stable.
|
|
46
|
+
}
|
|
47
|
+
reject(error());
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
52
|
+
Promise.resolve(operation).then((value) => finish(() => resolve(value)), (reason) => finish(() => reject(reason)));
|
|
53
|
+
if (signal.aborted)
|
|
54
|
+
abort();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function cancelWithoutWaiting(cancel) {
|
|
58
|
+
try {
|
|
59
|
+
return Promise.resolve(cancel()).catch(() => undefined);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// A stream may reject cancellation synchronously; the caller is already
|
|
63
|
+
// rejecting or returning a bounded result, so cleanup remains best effort.
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function releaseReaderWithoutThrowing(reader) {
|
|
68
|
+
try {
|
|
69
|
+
reader.releaseLock();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// A reader with a still-pending read can reject lock release. The bounded
|
|
73
|
+
// caller must retain its abort/size result rather than waiting for or
|
|
74
|
+
// replacing it with stream cleanup failure.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function readBoundedResponseBody(response, maxBytes, signal, options = {}) {
|
|
78
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
|
79
|
+
throw new Error("invalid response body limit");
|
|
80
|
+
}
|
|
81
|
+
const cancelBody = () => {
|
|
82
|
+
if (response.body)
|
|
83
|
+
cancelWithoutWaiting(() => response.body.cancel());
|
|
84
|
+
};
|
|
85
|
+
const cancelAbortedResponse = () => {
|
|
86
|
+
if (!response.body) {
|
|
87
|
+
cancelBody();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
let reader;
|
|
91
|
+
try {
|
|
92
|
+
reader = response.body.getReader();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
cancelBody();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const cancellation = cancelWithoutWaiting(() => reader.cancel());
|
|
99
|
+
if (options.deferReaderRelease)
|
|
100
|
+
void cancellation.then(() => releaseReaderWithoutThrowing(reader));
|
|
101
|
+
else
|
|
102
|
+
releaseReaderWithoutThrowing(reader);
|
|
103
|
+
};
|
|
104
|
+
if (signal?.aborted) {
|
|
105
|
+
cancelAbortedResponse();
|
|
106
|
+
throw options.abortError?.() ?? abortError();
|
|
107
|
+
}
|
|
108
|
+
const contentLength = response.headers.get("content-length");
|
|
109
|
+
if (contentLength !== null && (!/^\d+$/.test(contentLength) || Number(contentLength) > maxBytes)) {
|
|
110
|
+
cancelBody();
|
|
111
|
+
return { bytes: new Uint8Array(), truncated: true };
|
|
112
|
+
}
|
|
113
|
+
if (!response.body)
|
|
114
|
+
return { bytes: new Uint8Array(), truncated: false };
|
|
115
|
+
const reader = response.body.getReader();
|
|
116
|
+
let cancellation;
|
|
117
|
+
const cancelReader = () => {
|
|
118
|
+
cancellation ??= cancelWithoutWaiting(() => reader.cancel());
|
|
119
|
+
};
|
|
120
|
+
const releaseReader = () => {
|
|
121
|
+
if (options.deferReaderRelease && cancellation) {
|
|
122
|
+
void cancellation.then(() => releaseReaderWithoutThrowing(reader));
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
releaseReaderWithoutThrowing(reader);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const chunks = [];
|
|
129
|
+
let bytesRead = 0;
|
|
130
|
+
const readChunk = () => {
|
|
131
|
+
if (!signal)
|
|
132
|
+
return reader.read();
|
|
133
|
+
if (signal.aborted) {
|
|
134
|
+
cancelReader();
|
|
135
|
+
return Promise.reject(options.abortError?.() ?? abortError());
|
|
136
|
+
}
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
let settled = false;
|
|
139
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
140
|
+
const onAbort = () => {
|
|
141
|
+
if (settled)
|
|
142
|
+
return;
|
|
143
|
+
settled = true;
|
|
144
|
+
cancelReader();
|
|
145
|
+
cleanup();
|
|
146
|
+
reject(options.abortError?.() ?? abortError());
|
|
147
|
+
};
|
|
148
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
149
|
+
reader.read().then((result) => {
|
|
150
|
+
if (settled)
|
|
151
|
+
return;
|
|
152
|
+
settled = true;
|
|
153
|
+
cleanup();
|
|
154
|
+
resolve(result);
|
|
155
|
+
}, (error) => {
|
|
156
|
+
if (settled)
|
|
157
|
+
return;
|
|
158
|
+
settled = true;
|
|
159
|
+
cleanup();
|
|
160
|
+
reject(error);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
for (;;) {
|
|
166
|
+
const { done, value } = await readChunk();
|
|
167
|
+
if (done)
|
|
168
|
+
break;
|
|
169
|
+
const remaining = maxBytes - bytesRead;
|
|
170
|
+
if (value.byteLength > remaining) {
|
|
171
|
+
if (remaining > 0) {
|
|
172
|
+
const chunk = new Uint8Array(remaining);
|
|
173
|
+
chunk.set(value.subarray(0, remaining));
|
|
174
|
+
chunks.push(chunk);
|
|
175
|
+
bytesRead += remaining;
|
|
176
|
+
}
|
|
177
|
+
cancelReader();
|
|
178
|
+
return { bytes: joinBytes(chunks, bytesRead), truncated: true };
|
|
179
|
+
}
|
|
180
|
+
const chunk = new Uint8Array(value.byteLength);
|
|
181
|
+
chunk.set(value);
|
|
182
|
+
chunks.push(chunk);
|
|
183
|
+
bytesRead += value.byteLength;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
releaseReader();
|
|
188
|
+
}
|
|
189
|
+
return { bytes: joinBytes(chunks, bytesRead), truncated: false };
|
|
190
|
+
}
|
|
191
|
+
function joinBytes(chunks, total) {
|
|
192
|
+
const bytes = new Uint8Array(total);
|
|
193
|
+
let offset = 0;
|
|
194
|
+
for (const chunk of chunks) {
|
|
195
|
+
bytes.set(chunk, offset);
|
|
196
|
+
offset += chunk.byteLength;
|
|
197
|
+
}
|
|
198
|
+
return bytes;
|
|
199
|
+
}
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED