@telecrypt-io/storage 0.5.28 → 0.5.29

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.
@@ -3,7 +3,7 @@
3
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
- constructor(message: string);
6
+ constructor(message: string, options?: ErrorOptions);
7
7
  }
8
8
  /** Setup would replace account recovery state, which this SDK never does. */
9
9
  export declare class RecoveryAlreadyConfiguredError extends StorageError {
@@ -13,35 +13,35 @@ export declare class RecoveryAlreadyConfiguredError extends StorageError {
13
13
  /** Restore failed without exposing provider or crypto-library error details. */
14
14
  export declare class RecoveryRestoreError extends StorageError {
15
15
  readonly code: "RECOVERY_RESTORE_FAILED";
16
- constructor();
16
+ constructor(cause?: unknown);
17
17
  }
18
18
  /** Setup failed before the generated Recovery Key could be safely returned. */
19
19
  export declare class RecoverySetupError extends StorageError {
20
20
  readonly code: "RECOVERY_SETUP_FAILED";
21
- constructor();
21
+ constructor(cause?: unknown);
22
22
  }
23
23
  /** The recovery setup transport result is ambiguous; state must be checked before retrying. */
24
24
  export declare class RecoverySetupAmbiguousError extends StorageError {
25
25
  readonly code: "RECOVERY_SETUP_AMBIGUOUS";
26
- constructor();
26
+ constructor(cause?: unknown);
27
27
  }
28
28
  /** Recovery restore was cancelled after the provider may have changed state. */
29
29
  export declare class RecoveryRestoreAmbiguousError extends StorageError {
30
30
  readonly code: "RECOVERY_RESTORE_AMBIGUOUS";
31
- constructor();
31
+ constructor(cause?: unknown);
32
32
  }
33
33
  /** A Matrix mutation was cancelled after its request started. */
34
34
  export declare class MutationOutcomeUnknownError extends StorageError {
35
35
  readonly code: "MUTATION_OUTCOME_UNKNOWN";
36
36
  readonly operation: string;
37
- constructor(operation?: string);
37
+ constructor(operation?: string, options?: ErrorOptions);
38
38
  }
39
39
  /** A multi-room mutation committed some work before a later step failed. */
40
40
  export declare class MutationPartialError extends StorageError {
41
41
  readonly code: "MUTATION_PARTIAL";
42
42
  readonly operation: string;
43
43
  readonly completedIds: readonly string[];
44
- constructor(operation: string, completedIds: readonly string[], detail?: string);
44
+ constructor(operation: string, completedIds: readonly string[], detail?: string, options?: ErrorOptions);
45
45
  }
46
46
  /** A vault or folder still contains files or child folders. */
47
47
  export declare class NonEmptyTreeError extends StorageError {
@@ -53,13 +53,13 @@ export declare class NonEmptyTreeError extends StorageError {
53
53
  export declare class RoomCreationAmbiguousError extends StorageError {
54
54
  readonly code: "ROOM_CREATION_AMBIGUOUS";
55
55
  readonly operation: string;
56
- constructor(operation?: string);
56
+ constructor(operation?: string, cause?: unknown);
57
57
  }
58
58
  /** A newly-created room could not be fully rolled back. */
59
59
  export declare class RoomCleanupIncompleteError extends StorageError {
60
60
  readonly code: "ROOM_CLEANUP_INCOMPLETE";
61
61
  readonly roomId: string;
62
- constructor(roomId: string);
62
+ constructor(roomId: string, cause?: unknown);
63
63
  }
64
64
  /** The SDK's single product file-size limit was exceeded. */
65
65
  export declare class FileTooLargeError extends StorageError {
@@ -3,8 +3,8 @@
3
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
- constructor(message) {
7
- super(message);
6
+ constructor(message, options) {
7
+ super(message, options);
8
8
  this.name = "StorageError";
9
9
  }
10
10
  }
@@ -19,32 +19,32 @@ export class RecoveryAlreadyConfiguredError extends StorageError {
19
19
  /** Restore failed without exposing provider or crypto-library error details. */
20
20
  export class RecoveryRestoreError extends StorageError {
21
21
  code = "RECOVERY_RESTORE_FAILED";
22
- constructor() {
23
- super("recovery restore failed; verify the Recovery Key and account");
22
+ constructor(cause) {
23
+ super("recovery restore failed; verify the Recovery Key and account", cause === undefined ? undefined : { cause });
24
24
  this.name = "RecoveryRestoreError";
25
25
  }
26
26
  }
27
27
  /** Setup failed before the generated Recovery Key could be safely returned. */
28
28
  export class RecoverySetupError extends StorageError {
29
29
  code = "RECOVERY_SETUP_FAILED";
30
- constructor() {
31
- super("recovery setup did not complete; check recovery status before retrying");
30
+ constructor(cause) {
31
+ super("recovery setup did not complete; check recovery status before retrying", cause === undefined ? undefined : { cause });
32
32
  this.name = "RecoverySetupError";
33
33
  }
34
34
  }
35
35
  /** The recovery setup transport result is ambiguous; state must be checked before retrying. */
36
36
  export class RecoverySetupAmbiguousError extends StorageError {
37
37
  code = "RECOVERY_SETUP_AMBIGUOUS";
38
- constructor() {
39
- super("recovery setup may have completed; check recovery status before retrying");
38
+ constructor(cause) {
39
+ super("recovery setup may have completed; check recovery status before retrying", cause === undefined ? undefined : { cause });
40
40
  this.name = "RecoverySetupAmbiguousError";
41
41
  }
42
42
  }
43
43
  /** Recovery restore was cancelled after the provider may have changed state. */
44
44
  export class RecoveryRestoreAmbiguousError extends StorageError {
45
45
  code = "RECOVERY_RESTORE_AMBIGUOUS";
46
- constructor() {
47
- super("recovery restore may have completed; check recovery status before retrying");
46
+ constructor(cause) {
47
+ super("recovery restore may have completed; check recovery status before retrying", cause === undefined ? undefined : { cause });
48
48
  this.name = "RecoveryRestoreAmbiguousError";
49
49
  }
50
50
  }
@@ -52,8 +52,8 @@ export class RecoveryRestoreAmbiguousError extends StorageError {
52
52
  export class MutationOutcomeUnknownError extends StorageError {
53
53
  code = "MUTATION_OUTCOME_UNKNOWN";
54
54
  operation;
55
- constructor(operation = "operation") {
56
- super(`${operation} outcome is unknown; reconcile current state before retrying`);
55
+ constructor(operation = "operation", options) {
56
+ super(`${operation} outcome is unknown; reconcile current state before retrying`, options);
57
57
  this.name = "MutationOutcomeUnknownError";
58
58
  this.operation = operation;
59
59
  }
@@ -63,8 +63,8 @@ export class MutationPartialError extends StorageError {
63
63
  code = "MUTATION_PARTIAL";
64
64
  operation;
65
65
  completedIds;
66
- constructor(operation, completedIds, detail) {
67
- super(`${operation} failed after partial completion${detail ? `: ${detail}` : ""}; retry to reconcile current state`);
66
+ constructor(operation, completedIds, detail, options) {
67
+ super(`${operation} failed after partial completion${detail ? `: ${detail}` : ""}; retry to reconcile current state`, options);
68
68
  this.name = "MutationPartialError";
69
69
  this.operation = operation;
70
70
  this.completedIds = [...completedIds];
@@ -84,8 +84,8 @@ export class NonEmptyTreeError extends StorageError {
84
84
  export class RoomCreationAmbiguousError extends StorageError {
85
85
  code = "ROOM_CREATION_AMBIGUOUS";
86
86
  operation;
87
- constructor(operation = "room creation") {
88
- super(`${operation} outcome is unknown; reconcile newly-created rooms before retrying`);
87
+ constructor(operation = "room creation", cause) {
88
+ super(`${operation} outcome is unknown; reconcile newly-created rooms before retrying`, cause === undefined ? undefined : { cause });
89
89
  this.name = "RoomCreationAmbiguousError";
90
90
  this.operation = operation;
91
91
  }
@@ -94,8 +94,8 @@ export class RoomCreationAmbiguousError extends StorageError {
94
94
  export class RoomCleanupIncompleteError extends StorageError {
95
95
  code = "ROOM_CLEANUP_INCOMPLETE";
96
96
  roomId;
97
- constructor(roomId) {
98
- super("created room cleanup is incomplete; retry cleanup for the created room");
97
+ constructor(roomId, cause) {
98
+ super("created room cleanup is incomplete; retry cleanup for the created room", cause === undefined ? undefined : { cause });
99
99
  this.name = "RoomCleanupIncompleteError";
100
100
  this.roomId = roomId;
101
101
  }
@@ -1,24 +1,30 @@
1
+ /** Read a complete response body while preserving the caller's cancellation contract. */
2
+ export interface ResponseBody {
3
+ bytes: Uint8Array<ArrayBuffer>;
4
+ }
1
5
  /**
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.
6
+ * A response stream failed after producing some bytes. The bytes are kept for
7
+ * diagnostic callers; product-media callers must unwrap only the cause and
8
+ * never expose this raw buffer in a public error.
5
9
  */
6
- export interface BoundedResponseBody {
7
- bytes: Uint8Array<ArrayBuffer>;
8
- truncated: boolean;
10
+ export declare class ResponseBodyReadError extends Error {
11
+ readonly bytes: Uint8Array<ArrayBuffer>;
12
+ constructor(bytes: Uint8Array<ArrayBuffer>, cause: unknown);
9
13
  }
10
- export interface BoundedResponseOptions {
14
+ export interface ResponseBodyOptions {
11
15
  /** Error returned when the body signal aborts. */
12
16
  abortError?: () => Error;
13
- /** Keep a stream reader locked until its asynchronous cancellation settles. */
14
- deferReaderRelease?: boolean;
15
17
  }
16
18
  /**
17
19
  * Reject a caller-facing operation as soon as a signal aborts. The underlying
18
20
  * 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.
21
+ * implementations are allowed to ignore AbortSignal, but the SDK API must
22
+ * still settle at its deadline. A deadline rejection is the complete public
23
+ * outcome; the late operation has no second result channel.
22
24
  */
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>;
25
+ export declare function raceWithAbort<T>(operation: PromiseLike<T>, signal: AbortSignal, error: () => Error): Promise<T>;
26
+ /** Await cleanup for a response that is being rejected before its body is read. */
27
+ export declare function cancelResponseBody(response: Response, operation: string, primaryError?: unknown): Promise<void>;
28
+ export declare function readResponseBody(response: Response, signal?: AbortSignal, options?: ResponseBodyOptions): Promise<ResponseBody>;
29
+ /** Read successful product media with the SDK's single plaintext-size ceiling. */
30
+ export declare function readMediaResponseBody(response: Response, maxBytes: number, signal?: AbortSignal, options?: ResponseBodyOptions): Promise<ResponseBody>;
package/dist/core/http.js CHANGED
@@ -1,26 +1,32 @@
1
+ import { FileTooLargeError } from "./errors.js";
2
+ /**
3
+ * A response stream failed after producing some bytes. The bytes are kept for
4
+ * diagnostic callers; product-media callers must unwrap only the cause and
5
+ * never expose this raw buffer in a public error.
6
+ */
7
+ export class ResponseBodyReadError extends Error {
8
+ bytes;
9
+ constructor(bytes, cause) {
10
+ super("response body read failed", { cause });
11
+ this.name = "ResponseBodyReadError";
12
+ this.bytes = bytes;
13
+ }
14
+ }
15
+ const RESPONSE_CLEANUP_TIMEOUT_MS = 5_000;
1
16
  function abortError() {
2
17
  return new DOMException("The operation was aborted", "AbortError");
3
18
  }
4
19
  /**
5
20
  * Reject a caller-facing operation as soon as a signal aborts. The underlying
6
21
  * 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.
22
+ * implementations are allowed to ignore AbortSignal, but the SDK API must
23
+ * still settle at its deadline. A deadline rejection is the complete public
24
+ * outcome; the late operation has no second result channel.
10
25
  */
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
- }
26
+ export function raceWithAbort(operation, signal, error) {
27
+ // Attach the rejection observer before checking an already-aborted signal;
28
+ // the caller may have started this operation before entering the race.
29
+ const observedOperation = Promise.resolve(operation);
24
30
  return new Promise((resolve, reject) => {
25
31
  let settled = false;
26
32
  const cleanup = () => signal.removeEventListener("abort", abort);
@@ -32,98 +38,127 @@ export function raceWithAbort(operation, signal, onAbort, error) {
32
38
  callback();
33
39
  };
34
40
  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
- }
41
+ finish(() => {
42
+ try {
47
43
  reject(error());
48
- });
44
+ }
45
+ catch (abortError) {
46
+ reject(abortError);
47
+ }
49
48
  });
50
49
  };
51
50
  signal.addEventListener("abort", abort, { once: true });
52
- Promise.resolve(operation).then((value) => finish(() => resolve(value)), (reason) => finish(() => reject(reason)));
51
+ observedOperation.then((value) => finish(() => resolve(value)), (reason) => finish(() => reject(reason)));
53
52
  if (signal.aborted)
54
53
  abort();
55
54
  });
56
55
  }
57
- function cancelWithoutWaiting(cancel) {
56
+ function combineCleanupFailures(primaryError, cleanupFailures, message) {
57
+ const errors = primaryError === undefined ? [...cleanupFailures] : [primaryError, ...cleanupFailures];
58
+ return new AggregateError(errors, message, {
59
+ cause: primaryError ?? cleanupFailures[0],
60
+ });
61
+ }
62
+ async function runBoundedCleanup(cleanup, operation) {
63
+ const pending = Promise.resolve().then(cleanup);
64
+ let timer;
65
+ const deadline = new Promise((_, reject) => {
66
+ timer = setTimeout(() => {
67
+ reject(new Error(`${operation} cleanup timed out`));
68
+ }, RESPONSE_CLEANUP_TIMEOUT_MS);
69
+ });
70
+ try {
71
+ await Promise.race([pending, deadline]);
72
+ }
73
+ finally {
74
+ if (timer !== undefined)
75
+ clearTimeout(timer);
76
+ }
77
+ }
78
+ /** Await cleanup for a response that is being rejected before its body is read. */
79
+ export async function cancelResponseBody(response, operation, primaryError) {
80
+ if (!response.body)
81
+ return;
58
82
  try {
59
- return Promise.resolve(cancel()).catch(() => undefined);
83
+ await runBoundedCleanup(() => response.body.cancel(), `${operation} response`);
60
84
  }
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();
85
+ catch (error) {
86
+ throw combineCleanupFailures(primaryError, [error], primaryError === undefined
87
+ ? `${operation} response cleanup was incomplete`
88
+ : `${operation} failed and response cleanup was incomplete`);
65
89
  }
66
90
  }
67
- function releaseReaderWithoutThrowing(reader) {
91
+ async function cancelReaderAndRelease(reader, operation, primaryError) {
92
+ const cleanupFailures = [];
93
+ try {
94
+ await runBoundedCleanup(() => reader.cancel(), `${operation} reader cancellation`);
95
+ }
96
+ catch (error) {
97
+ cleanupFailures.push(error);
98
+ }
99
+ // Cancellation is awaited before releasing the reader, so cleanup failures
100
+ // stay part of the current operation instead of mutating a later error.
68
101
  try {
69
102
  reader.releaseLock();
70
103
  }
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.
104
+ catch (error) {
105
+ cleanupFailures.push(error);
75
106
  }
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");
107
+ if (cleanupFailures.length > 0) {
108
+ throw combineCleanupFailures(primaryError, cleanupFailures, primaryError === undefined
109
+ ? `${operation} reader cleanup was incomplete`
110
+ : `${operation} failed and reader cleanup was incomplete`);
80
111
  }
81
- const cancelBody = () => {
82
- if (response.body)
83
- cancelWithoutWaiting(() => response.body.cancel());
84
- };
85
- const cancelAbortedResponse = () => {
86
- if (!response.body) {
87
- cancelBody();
112
+ }
113
+ async function readResponseBodyInternal(response, signal, options = {}, maxBytes) {
114
+ const cancelAbortedResponse = async (failure) => {
115
+ if (!response.body)
88
116
  return;
89
- }
90
117
  let reader;
91
118
  try {
92
119
  reader = response.body.getReader();
93
120
  }
94
- catch {
95
- cancelBody();
96
- return;
121
+ catch (error) {
122
+ throw combineCleanupFailures(failure, [error], "response reader setup failed after abort");
97
123
  }
98
- const cancellation = cancelWithoutWaiting(() => reader.cancel());
99
- if (options.deferReaderRelease)
100
- void cancellation.then(() => releaseReaderWithoutThrowing(reader));
101
- else
102
- releaseReaderWithoutThrowing(reader);
124
+ await cancelReaderAndRelease(reader, "response body", failure);
103
125
  };
104
126
  if (signal?.aborted) {
105
- cancelAbortedResponse();
106
- throw options.abortError?.() ?? abortError();
127
+ const failure = options.abortError?.() ?? abortError();
128
+ await cancelAbortedResponse(failure);
129
+ throw failure;
107
130
  }
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 };
131
+ if (maxBytes !== undefined &&
132
+ (!Number.isSafeInteger(maxBytes) || maxBytes < 0)) {
133
+ throw new Error("invalid media response body limit");
134
+ }
135
+ if (maxBytes !== undefined) {
136
+ const contentLength = response.headers.get("content-length");
137
+ if (contentLength !== null && /^\d+$/u.test(contentLength) && Number(contentLength) > maxBytes) {
138
+ const tooLarge = new FileTooLargeError();
139
+ await cancelResponseBody(response, "media response", tooLarge);
140
+ throw tooLarge;
141
+ }
112
142
  }
113
143
  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));
144
+ return { bytes: new Uint8Array() };
145
+ let reader;
146
+ try {
147
+ reader = response.body.getReader();
148
+ }
149
+ catch (error) {
150
+ let failure = error;
151
+ try {
152
+ await cancelResponseBody(response, "response body reader setup", error);
123
153
  }
124
- else {
125
- releaseReaderWithoutThrowing(reader);
154
+ catch (cleanupError) {
155
+ failure = cleanupError;
126
156
  }
157
+ throw new ResponseBodyReadError(new Uint8Array(), failure);
158
+ }
159
+ let abortCancellation;
160
+ const cancelReader = () => {
161
+ abortCancellation ??= runBoundedCleanup(() => reader.cancel(), "response body reader");
127
162
  };
128
163
  const chunks = [];
129
164
  let bytesRead = 0;
@@ -166,16 +201,8 @@ export async function readBoundedResponseBody(response, maxBytes, signal, option
166
201
  const { done, value } = await readChunk();
167
202
  if (done)
168
203
  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 };
204
+ if (maxBytes !== undefined && value.byteLength > maxBytes - bytesRead) {
205
+ throw new FileTooLargeError();
179
206
  }
180
207
  const chunk = new Uint8Array(value.byteLength);
181
208
  chunk.set(value);
@@ -183,10 +210,57 @@ export async function readBoundedResponseBody(response, maxBytes, signal, option
183
210
  bytesRead += value.byteLength;
184
211
  }
185
212
  }
186
- finally {
187
- releaseReader();
213
+ catch (error) {
214
+ if (signal?.aborted) {
215
+ const failure = new ResponseBodyReadError(joinBytes(chunks, bytesRead), error);
216
+ const cleanupFailures = [];
217
+ if (abortCancellation) {
218
+ try {
219
+ await abortCancellation;
220
+ }
221
+ catch (cleanupError) {
222
+ cleanupFailures.push(cleanupError);
223
+ }
224
+ }
225
+ try {
226
+ reader.releaseLock();
227
+ }
228
+ catch (cleanupError) {
229
+ cleanupFailures.push(cleanupError);
230
+ }
231
+ if (cleanupFailures.length > 0) {
232
+ throw combineCleanupFailures(failure, cleanupFailures, "response body read failed and reader cleanup was incomplete");
233
+ }
234
+ throw failure;
235
+ }
236
+ try {
237
+ await cancelReaderAndRelease(reader, "response body", error);
238
+ }
239
+ catch (cleanupError) {
240
+ if (error instanceof FileTooLargeError)
241
+ throw cleanupError;
242
+ throw new ResponseBodyReadError(joinBytes(chunks, bytesRead), cleanupError);
243
+ }
244
+ if (error instanceof FileTooLargeError)
245
+ throw error;
246
+ throw new ResponseBodyReadError(joinBytes(chunks, bytesRead), error);
188
247
  }
189
- return { bytes: joinBytes(chunks, bytesRead), truncated: false };
248
+ // A settled normal read has no pending read request, so Web Streams
249
+ // guarantees that this synchronous release cannot throw.
250
+ try {
251
+ reader.releaseLock();
252
+ }
253
+ catch (error) {
254
+ throw new ResponseBodyReadError(joinBytes(chunks, bytesRead), error);
255
+ }
256
+ return { bytes: joinBytes(chunks, bytesRead) };
257
+ }
258
+ export function readResponseBody(response, signal, options = {}) {
259
+ return readResponseBodyInternal(response, signal, options);
260
+ }
261
+ /** Read successful product media with the SDK's single plaintext-size ceiling. */
262
+ export function readMediaResponseBody(response, maxBytes, signal, options = {}) {
263
+ return readResponseBodyInternal(response, signal, options, maxBytes);
190
264
  }
191
265
  function joinBytes(chunks, total) {
192
266
  const bytes = new Uint8Array(total);
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Shared OIDC/MAS login building blocks — used by BOTH the CLI (device-code
3
+ * grant) and the web UI (authorization-code + PKCE). Browser-safe (no
4
+ * node:fs/path/v8/process/commander/fake-indexeddb): browser state is only
5
+ * touched when the PKCE functions are called by the web adapter. Importing
6
+ * this module under Node is safe; the CLI uses the device-code functions,
7
+ * which are plain fetch calls.
8
+ */
1
9
  import { type DeviceAuthorizationResponse, type DeviceAccessTokenResponse, type DeviceAccessTokenError, type BearerTokenResponse as OAuthBearerTokenResponse, type ValidatedAuthMetadata } from "matrix-js-sdk/lib/oauth/index.js";
2
10
  import type { AccessTokens, TokenRefreshFunction } from "matrix-js-sdk/lib/http-api/index.js";
3
11
  /** Matrix 1.18 OAuth metadata returned by `/auth_metadata`. */
@@ -22,6 +30,13 @@ export interface OidcRegistrationClientMetadata {
22
30
  }
23
31
  /** The issuer-bound endpoints accepted by the token refresh adapter. */
24
32
  export type OidcTokenEndpointMetadata = Pick<OidcClientConfig, "issuer" | "token_endpoint" | "revocation_endpoint">;
33
+ /** Keep provider diagnostics useful without allowing credentials, customer
34
+ * identifiers, or control characters from an untrusted response to cross the
35
+ * SDK boundary. */
36
+ export declare function sanitizeDiagnosticText(value: string): string;
37
+ /** Copy a transport error's useful details across the SDK boundary after the
38
+ * same secret/control-character sanitization used for provider responses. */
39
+ export declare function diagnosticCause(error: unknown, seen?: WeakSet<object>): Error;
25
40
  /**
26
41
  * Discovers the OIDC/MAS issuer and endpoints for a homeserver via the
27
42
  * Matrix client's stable `/auth_metadata` endpoint. Matrix 42 performs this