@telecrypt-io/storage 0.6.1 → 0.7.1

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 CHANGED
@@ -5,21 +5,21 @@
5
5
  End-to-end encrypted file storage and sharing, built on Matrix.
6
6
 
7
7
  The current public TeleCrypt architecture, responsibilities, and product limits are authoritative at
8
- [telecrypt.io/llms.txt](https://telecrypt.io/llms.txt). This README documents this SDK's package API
8
+ [telecrypt-io.github.io/llms-authority/llms.txt](https://telecrypt-io.github.io/llms-authority/llms.txt). This README documents this SDK's package API
9
9
  and development workflow.
10
10
 
11
11
  Files are encrypted on the client before upload. The server stores only opaque ciphertext and
12
12
  never holds the decryption keys. Shared vaults let multiple people add and read files, and a
13
13
  Recovery Key restores your files on a new device — even if you lose the original.
14
14
 
15
- This repository publishes the library package from annotated exact-version tags whose resulting
16
- GitHub Release is immutable, with NPM Trusted Publishing/OIDC provenance. The command-line client
17
- is maintained in its own repository.
15
+ This repository publishes the library package from annotated version tags using npm Trusted
16
+ Publishing. GitHub Releases identify the corresponding source version. The command-line client is
17
+ maintained in its own repository.
18
18
 
19
19
  ## Install
20
20
 
21
21
  ```bash
22
- npm install @telecrypt-io/storage@0.6.0
22
+ npm install @telecrypt-io/storage
23
23
  ```
24
24
 
25
25
  This gives you the `TeleCryptIOStorage` library and its browser-safe `core` API.
@@ -120,15 +120,12 @@ a production server. This is the shared fixture for the SDK, CLI, and Web functi
120
120
  start it from this checkout and reuse it for the sibling repositories. Unit tests may use mocks for
121
121
  isolated boundaries, but they do not replace the real-stack checks.
122
122
 
123
- If setup or tests fail, preserve the fixture and its diagnostics; do not run
124
- `npm run synapse:down` before the private Harness investigation is complete. The fixture scripts do
125
- not provide that workflow; follow the [Harness operator workflow](https://github.com/TeleCrypt-io/Harness/blob/main/docs/release.md#required-stage-first-sequence)
126
- for the canonical ordering and stopping boundary.
123
+ If setup or tests fail, inspect the relevant logs, then run `npm run synapse:down` when finished.
127
124
 
128
125
  After a successful run, use `npm run synapse:down` for scoped teardown. Use
129
126
  `./throwaway_synapse/down.sh --wipe` only when a complete fixture reset is explicitly required.
130
127
 
131
- See [RELEASING.md](./RELEASING.md) for the guarded npm release procedure.
128
+ See [RELEASING.md](./RELEASING.md) for the npm release procedure.
132
129
 
133
130
  ## Licence
134
131
 
@@ -115,7 +115,7 @@ export interface CreateFromOidcOptions {
115
115
  /** Waits for one Matrix mutation to settle without aborting the shared client.
116
116
  * If cancellation arrives after the mutation starts, the outcome is explicitly
117
117
  * unknown: the caller must reconcile and may safely retry idempotent work. */
118
- export declare function withMatrixMutationAbort<T>(_client: MatrixClient, operation: () => Promise<T>, signal?: AbortSignal, operationName?: string): Promise<T>;
118
+ export declare function withMatrixMutationAbort<T>(operation: () => Promise<T>, signal?: AbortSignal, operationName?: string): Promise<T>;
119
119
  export declare function boundedMatrixFetch(fetchFn: typeof fetch): typeof fetch;
120
120
  export interface MatrixRequestOptions {
121
121
  signal?: AbortSignal;
@@ -4,7 +4,7 @@ import { ClientPrefix } from "matrix-js-sdk/lib/http-api/prefix.js";
4
4
  import { encryptAttachment, decryptAttachment } from "matrix-encrypt-attachment";
5
5
  import { decodeRecoveryKey } from "matrix-js-sdk/lib/crypto-api/recovery-key.js";
6
6
  import { FileTooLargeError, RecoveryAlreadyConfiguredError, RecoveryRestoreError, RecoverySetupAmbiguousError, RecoverySetupError, RecoveryRestoreAmbiguousError, MutationOutcomeUnknownError, RoomCreationAmbiguousError, RoomCleanupIncompleteError, StorageError, UndecryptableFileError, } from "./core/errors.js";
7
- import { MAX_MEDIA_FILE_BYTES, MAX_MATRIX_IDENTIFIER_LENGTH, validateCanonicalMatrixUserId, validateMatrixDeviceId, validateMatrixEventId, validateMatrixRoomId, } from "./core/constants.js";
7
+ import { MAX_MEDIA_FILE_BYTES, MAX_MATRIX_IDENTIFIER_BYTES, validateCanonicalMatrixUserId, validateMatrixDeviceId, validateMatrixEventId, validateMatrixRoomId, } from "./core/constants.js";
8
8
  import { cancelResponseBody, raceWithAbort, readMediaResponseBody, readResponseBody, ResponseBodyReadError, } from "./core/http.js";
9
9
  import { sanitizeDiagnosticText } from "./core/oidc.js";
10
10
  import { validateName } from "./core/validation.js";
@@ -16,9 +16,6 @@ const MEDIA_TIMEOUT_MS = 30000;
16
16
  const MATRIX_HTTP_TIMEOUT_MS = 30000;
17
17
  const CLEANUP_TIMEOUT_MS = 30000;
18
18
  const RECOVERY_CRYPTO_TIMEOUT_MS = 60000;
19
- const MAX_MEDIA_REDIRECTS = 5;
20
- const MAX_MATRIX_TOKEN_LENGTH = 8192;
21
- const MAX_MIMETYPE_LENGTH = 255;
22
19
  function cloneCryptoCallbacks(callbacks) {
23
20
  // MatrixClient retains this object for the lifetime of the client. Keep the
24
21
  // caller's object immutable: recovery temporarily installs key callbacks on
@@ -29,7 +26,7 @@ function cloneCryptoCallbacks(callbacks) {
29
26
  /** Waits for one Matrix mutation to settle without aborting the shared client.
30
27
  * If cancellation arrives after the mutation starts, the outcome is explicitly
31
28
  * unknown: the caller must reconcile and may safely retry idempotent work. */
32
- export function withMatrixMutationAbort(_client, operation, signal, operationName = "Matrix mutation") {
29
+ export function withMatrixMutationAbort(operation, signal, operationName = "Matrix mutation") {
33
30
  if (!signal)
34
31
  return operation();
35
32
  if (signal.aborted)
@@ -114,7 +111,7 @@ export function boundedMatrixFetch(fetchFn) {
114
111
  response.type === "opaqueredirect" ||
115
112
  (response.status >= 300 && response.status < 400)) {
116
113
  const failure = new Error("Matrix redirect rejected");
117
- await cancelResponseBody(response, "Matrix redirect", failure);
114
+ cancelResponseBody(response);
118
115
  throw failure;
119
116
  }
120
117
  return await readMatrixResponse(response, controller.signal, () => (timedOut ? new Error("Matrix request timed out") : new DOMException("The operation was aborted", "AbortError")));
@@ -151,7 +148,6 @@ function isLoopbackHostname(hostname) {
151
148
  function validateHomeserverUrl(value) {
152
149
  if (typeof value !== "string" ||
153
150
  value.length === 0 ||
154
- value.length > 2048 ||
155
151
  value.trim() !== value ||
156
152
  /[\s\u0000-\u001f\u007f]/.test(value)) {
157
153
  throw new Error("invalid Matrix homeserver URL");
@@ -177,7 +173,6 @@ function validateHomeserverUrl(value) {
177
173
  function validateMatrixToken(value) {
178
174
  if (typeof value !== "string" ||
179
175
  value.length === 0 ||
180
- value.length > MAX_MATRIX_TOKEN_LENGTH ||
181
176
  value.trim() !== value ||
182
177
  /[\s\u0000-\u001f\u007f]/.test(value)) {
183
178
  throw new Error("invalid Matrix token");
@@ -186,7 +181,7 @@ function validateMatrixToken(value) {
186
181
  function validateMatrixIdentifier(value, name) {
187
182
  if (typeof value !== "string" ||
188
183
  value.length === 0 ||
189
- value.length > MAX_MATRIX_IDENTIFIER_LENGTH ||
184
+ new TextEncoder().encode(value).byteLength > MAX_MATRIX_IDENTIFIER_BYTES ||
190
185
  /[\s\u0000-\u001f\u007f]/.test(value)) {
191
186
  throw new Error(`invalid Matrix ${name}`);
192
187
  }
@@ -197,7 +192,6 @@ function validateFileName(value) {
197
192
  function validateMimetype(value) {
198
193
  if (typeof value !== "string" ||
199
194
  value.length === 0 ||
200
- value.length > MAX_MIMETYPE_LENGTH ||
201
195
  /[\u0000-\u001f\u007f]/.test(value) ||
202
196
  !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+(?:\s*;\s*[!#$%&'*+.^_`|~0-9A-Za-z-]+=[^;\r\n]+)*$/.test(value)) {
203
197
  throw new Error("invalid MIME type");
@@ -354,7 +348,7 @@ function parseMatrixStateResponse(value) {
354
348
  throw new Error("invalid Matrix room state response");
355
349
  }
356
350
  validateMatrixIdentifier(event.type, "state event type");
357
- if (event.state_key.length > MAX_MATRIX_IDENTIFIER_LENGTH ||
351
+ if (new TextEncoder().encode(event.state_key).byteLength > MAX_MATRIX_IDENTIFIER_BYTES ||
358
352
  /[\s\u0000-\u001f\u007f]/.test(event.state_key)) {
359
353
  throw new Error("invalid Matrix state event key");
360
354
  }
@@ -391,30 +385,8 @@ function isGoneRoomError(error) {
391
385
  (error.errcode === "M_NOT_FOUND" || error.errcode === "M_UNKNOWN"));
392
386
  }
393
387
  function throwWithCleanupDetail(error, roomId, cleanupError) {
394
- const cleanupCause = cleanupError === undefined
395
- ? error
396
- : new AggregateError([error, cleanupError], "operation and room cleanup failed", { cause: error });
397
- const cleanup = new RoomCleanupIncompleteError(roomId, cleanupCause);
398
- if (error instanceof Error) {
399
- try {
400
- Object.defineProperty(error, "cleanupIncomplete", {
401
- value: true,
402
- enumerable: false,
403
- });
404
- Object.defineProperty(error, "cleanupError", {
405
- value: cleanup,
406
- enumerable: false,
407
- });
408
- }
409
- catch {
410
- // A non-extensible provider error cannot carry the cleanup marker. Throw
411
- // the existing typed cleanup error and retain the provider error as cause
412
- // so callers cannot mistake the mutation for a safe retry.
413
- throw cleanup;
414
- }
415
- throw error;
416
- }
417
- throw cleanup;
388
+ const cause = new AggregateError([error, cleanupError], "operation and room cleanup failed", { cause: error });
389
+ throw new RoomCleanupIncompleteError(roomId, cause);
418
390
  }
419
391
  /**
420
392
  * Serializes all tree mutations for one MatrixClient. A single queue is
@@ -996,7 +968,6 @@ export class TeleCryptIOStorage {
996
968
  const crypto = this.requireCrypto();
997
969
  if (typeof recoveryKey !== "string" ||
998
970
  recoveryKey.length === 0 ||
999
- recoveryKey.length > 256 ||
1000
971
  /[^A-Za-z0-9 ]/.test(recoveryKey)) {
1001
972
  throw new RecoveryRestoreError();
1002
973
  }
@@ -1150,19 +1121,17 @@ export class TeleCryptIOStorage {
1150
1121
  return tree;
1151
1122
  }
1152
1123
  /** Cleans up only a room created by this operation and reports incomplete cleanup. */
1153
- async cleanupCreatedRoom(roomId, _signal) {
1124
+ async cleanupCreatedRoom(roomId) {
1154
1125
  // Cleanup is compensating work. It must not inherit the caller's already
1155
1126
  // aborted deadline, or a timed-out create could never even attempt leave.
1156
1127
  const cleanupSignal = new AbortController().signal;
1157
- let incomplete = false;
1158
1128
  let safeToForget = true;
1159
1129
  const failures = [];
1160
1130
  try {
1161
- await TeleCryptIOStorage.withTimeout(withMatrixMutationAbort(this.client, () => this.client.leave(roomId), cleanupSignal), CLEANUP_TIMEOUT_MS, "room leave cleanup", cleanupSignal);
1131
+ await TeleCryptIOStorage.withTimeout(withMatrixMutationAbort(() => this.client.leave(roomId), cleanupSignal), CLEANUP_TIMEOUT_MS, "room leave cleanup", cleanupSignal);
1162
1132
  }
1163
1133
  catch (error) {
1164
1134
  if (!isGoneRoomError(error)) {
1165
- incomplete = true;
1166
1135
  failures.push(error);
1167
1136
  // A timed-out or failed leave may still be in flight. Do not race a
1168
1137
  // forget request against it; the caller must retry cleanup later.
@@ -1171,16 +1140,15 @@ export class TeleCryptIOStorage {
1171
1140
  }
1172
1141
  if (safeToForget) {
1173
1142
  try {
1174
- await TeleCryptIOStorage.withTimeout(withMatrixMutationAbort(this.client, () => this.client.forget(roomId), cleanupSignal), CLEANUP_TIMEOUT_MS, "room forget cleanup", cleanupSignal);
1143
+ await TeleCryptIOStorage.withTimeout(withMatrixMutationAbort(() => this.client.forget(roomId), cleanupSignal), CLEANUP_TIMEOUT_MS, "room forget cleanup", cleanupSignal);
1175
1144
  }
1176
1145
  catch (error) {
1177
1146
  if (!isGoneRoomError(error)) {
1178
- incomplete = true;
1179
1147
  failures.push(error);
1180
1148
  }
1181
1149
  }
1182
1150
  }
1183
- if (incomplete) {
1151
+ if (failures.length > 0) {
1184
1152
  const cause = failures.length === 1
1185
1153
  ? failures[0]
1186
1154
  : new AggregateError(failures, "room cleanup failed");
@@ -1203,7 +1171,7 @@ export class TeleCryptIOStorage {
1203
1171
  if (!currentUserId || typeof getSender !== "function" || getSender() !== currentUserId)
1204
1172
  return false;
1205
1173
  const eventId = validateMatrixEventId(event.getId(), "state event ID");
1206
- await withMatrixMutationAbort(this.client, () => this.client.redactEvent(roomId, eventId), signal);
1174
+ await withMatrixMutationAbort(() => this.client.redactEvent(roomId, eventId), signal);
1207
1175
  return true;
1208
1176
  }
1209
1177
  async removeChildLink(parentId, childId, signal) {
@@ -1215,7 +1183,7 @@ export class TeleCryptIOStorage {
1215
1183
  async createTreeSpace(name, operation, signal) {
1216
1184
  if (signal?.aborted)
1217
1185
  throw new StorageError("operation cancelled");
1218
- const response = await TeleCryptIOStorage.withMutationTimeout(withMatrixMutationAbort(this.client, () => this.client.createRoom(this.treeRoomOptions(name)), signal, `${operation} room creation`), MATRIX_HTTP_TIMEOUT_MS, `${operation} room creation`).catch((error) => {
1186
+ const response = await TeleCryptIOStorage.withMutationTimeout(withMatrixMutationAbort(() => this.client.createRoom(this.treeRoomOptions(name)), signal, `${operation} room creation`), MATRIX_HTTP_TIMEOUT_MS, `${operation} room creation`).catch((error) => {
1219
1187
  // createRoom is a server-side mutation. A rejected HTTP promise does
1220
1188
  // not prove that the request was rejected before commit, so every
1221
1189
  // non-validation failure follows the reconciliation path instead of a
@@ -1236,7 +1204,7 @@ export class TeleCryptIOStorage {
1236
1204
  }
1237
1205
  catch (error) {
1238
1206
  try {
1239
- await this.cleanupCreatedRoom(roomId, signal);
1207
+ await this.cleanupCreatedRoom(roomId);
1240
1208
  }
1241
1209
  catch (cleanupError) {
1242
1210
  throwWithCleanupDetail(error, roomId, cleanupError);
@@ -1288,12 +1256,12 @@ export class TeleCryptIOStorage {
1288
1256
  if (!via)
1289
1257
  throw new Error("createSubtree: Matrix client has no server domain");
1290
1258
  parentLinkAttempted = true;
1291
- const parentLink = await TeleCryptIOStorage.withMutationTimeout(withMatrixMutationAbort(this.client, () => this.client.sendStateEvent(currentParent.id, EventType.SpaceChild, { via: [via] }, tree.id), effectiveSignal), MATRIX_HTTP_TIMEOUT_MS, "parent link");
1259
+ const parentLink = await TeleCryptIOStorage.withMutationTimeout(withMatrixMutationAbort(() => this.client.sendStateEvent(currentParent.id, EventType.SpaceChild, { via: [via] }, tree.id), effectiveSignal), MATRIX_HTTP_TIMEOUT_MS, "parent link");
1292
1260
  if (parentLink?.event_id !== undefined) {
1293
1261
  parentLinkEventId = validateMatrixEventId(parentLink.event_id, "parent link response event ID");
1294
1262
  }
1295
1263
  childLinkAttempted = true;
1296
- const childLink = await TeleCryptIOStorage.withMutationTimeout(withMatrixMutationAbort(this.client, () => this.client.sendStateEvent(tree.id, EventType.SpaceParent, { via: [via] }, currentParent.id), effectiveSignal), MATRIX_HTTP_TIMEOUT_MS, "child link");
1264
+ const childLink = await TeleCryptIOStorage.withMutationTimeout(withMatrixMutationAbort(() => this.client.sendStateEvent(tree.id, EventType.SpaceParent, { via: [via] }, currentParent.id), effectiveSignal), MATRIX_HTTP_TIMEOUT_MS, "child link");
1297
1265
  if (childLink?.event_id !== undefined) {
1298
1266
  childLinkEventId = validateMatrixEventId(childLink.event_id, "child link response event ID");
1299
1267
  }
@@ -1311,7 +1279,6 @@ export class TeleCryptIOStorage {
1311
1279
  // Reconcile and retry these idempotent relation updates with a fresh
1312
1280
  // signal, then report incomplete cleanup if they cannot be verified.
1313
1281
  const compensationSignal = new AbortController().signal;
1314
- let rollbackIncomplete = false;
1315
1282
  const cleanupFailures = [];
1316
1283
  // A sendStateEvent rejection is ambiguous: the homeserver may have
1317
1284
  // committed the event before the client observed a transport error.
@@ -1322,7 +1289,7 @@ export class TeleCryptIOStorage {
1322
1289
  if (parentLinkAttempted) {
1323
1290
  try {
1324
1291
  if (parentLinkEventId) {
1325
- await TeleCryptIOStorage.withTimeout(withMatrixMutationAbort(this.client, () => this.client.redactEvent(currentParent.id, parentLinkEventId), compensationSignal), MATRIX_HTTP_TIMEOUT_MS, "parent link rollback", compensationSignal);
1292
+ await TeleCryptIOStorage.withTimeout(withMatrixMutationAbort(() => this.client.redactEvent(currentParent.id, parentLinkEventId), compensationSignal), MATRIX_HTTP_TIMEOUT_MS, "parent link rollback", compensationSignal);
1326
1293
  await this.refreshRoomState(currentParent.id, { signal: compensationSignal });
1327
1294
  const remaining = this.client
1328
1295
  .getRoom(currentParent.id)
@@ -1335,7 +1302,6 @@ export class TeleCryptIOStorage {
1335
1302
  !Array.isArray(content) &&
1336
1303
  Object.keys(content).length > 0);
1337
1304
  })) {
1338
- rollbackIncomplete = true;
1339
1305
  cleanupFailures.push(new Error("parent link rollback could not be verified"));
1340
1306
  }
1341
1307
  }
@@ -1345,20 +1311,18 @@ export class TeleCryptIOStorage {
1345
1311
  // Without an event ID, a missing link after one refresh does not
1346
1312
  // prove that a request which timed out will never commit.
1347
1313
  if (!removed) {
1348
- rollbackIncomplete = true;
1349
1314
  cleanupFailures.push(new Error("parent link rollback could not be verified"));
1350
1315
  }
1351
1316
  }
1352
1317
  }
1353
1318
  catch (cleanupError) {
1354
- rollbackIncomplete = true;
1355
1319
  cleanupFailures.push(cleanupError);
1356
1320
  }
1357
1321
  }
1358
1322
  if (childLinkAttempted) {
1359
1323
  try {
1360
1324
  if (childLinkEventId) {
1361
- await TeleCryptIOStorage.withTimeout(withMatrixMutationAbort(this.client, () => this.client.redactEvent(tree.id, childLinkEventId), compensationSignal), MATRIX_HTTP_TIMEOUT_MS, "child link rollback", compensationSignal);
1325
+ await TeleCryptIOStorage.withTimeout(withMatrixMutationAbort(() => this.client.redactEvent(tree.id, childLinkEventId), compensationSignal), MATRIX_HTTP_TIMEOUT_MS, "child link rollback", compensationSignal);
1362
1326
  await this.refreshRoomState(tree.id, { signal: compensationSignal });
1363
1327
  const remaining = this.client
1364
1328
  .getRoom(tree.id)
@@ -1371,7 +1335,6 @@ export class TeleCryptIOStorage {
1371
1335
  !Array.isArray(content) &&
1372
1336
  Object.keys(content).length > 0);
1373
1337
  })) {
1374
- rollbackIncomplete = true;
1375
1338
  cleanupFailures.push(new Error("child link rollback could not be verified"));
1376
1339
  }
1377
1340
  }
@@ -1379,23 +1342,21 @@ export class TeleCryptIOStorage {
1379
1342
  await this.refreshRoomState(tree.id, { signal: compensationSignal });
1380
1343
  const removed = await this.removeParentLink(tree.id, currentParent.id, compensationSignal);
1381
1344
  if (!removed) {
1382
- rollbackIncomplete = true;
1383
1345
  cleanupFailures.push(new Error("child link rollback could not be verified"));
1384
1346
  }
1385
1347
  }
1386
1348
  }
1387
1349
  catch (cleanupError) {
1388
- rollbackIncomplete = true;
1389
1350
  cleanupFailures.push(cleanupError);
1390
1351
  }
1391
1352
  }
1392
1353
  try {
1393
- await this.cleanupCreatedRoom(tree.id, effectiveSignal);
1354
+ await this.cleanupCreatedRoom(tree.id);
1394
1355
  }
1395
1356
  catch (cleanupError) {
1396
1357
  cleanupFailures.push(cleanupError);
1397
1358
  }
1398
- if (rollbackIncomplete || cleanupFailures.length > 0) {
1359
+ if (cleanupFailures.length > 0) {
1399
1360
  const cleanupCause = cleanupFailures.length === 1
1400
1361
  ? cleanupFailures[0]
1401
1362
  : new AggregateError(cleanupFailures, "tree creation cleanup failed");
@@ -1489,7 +1450,7 @@ export class TeleCryptIOStorage {
1489
1450
  throw new StorageError("operation cancelled");
1490
1451
  let response;
1491
1452
  try {
1492
- response = await withMatrixMutationAbort(this.client, () => tree.createFile(name, new Uint8Array(encrypted.data), encrypted.info, { info: { mimetype, size: data.byteLength } }), signal, "file upload");
1453
+ response = await withMatrixMutationAbort(() => tree.createFile(name, new Uint8Array(encrypted.data), encrypted.info, { info: { mimetype, size: data.byteLength } }), signal, "file upload");
1493
1454
  }
1494
1455
  catch (error) {
1495
1456
  // A transport failure after the encrypted event was submitted cannot be
@@ -1587,7 +1548,7 @@ export class TeleCryptIOStorage {
1587
1548
  signal?.addEventListener("abort", abortExternal, { once: true });
1588
1549
  let ciphertext;
1589
1550
  try {
1590
- for (let redirect = 0;; redirect += 1) {
1551
+ for (;;) {
1591
1552
  const res = await raceWithAbort(fetch(currentUrl, {
1592
1553
  redirect: "manual",
1593
1554
  signal: controller.signal,
@@ -1598,33 +1559,28 @@ export class TeleCryptIOStorage {
1598
1559
  if ([301, 302, 303, 307, 308].includes(res.status)) {
1599
1560
  let redirectError;
1600
1561
  let nextUrl;
1601
- if (redirect >= MAX_MEDIA_REDIRECTS) {
1602
- redirectError = new Error("media download redirect limit exceeded");
1562
+ const location = res.headers.get("location");
1563
+ if (!location) {
1564
+ redirectError = new Error("media download redirect missing location");
1603
1565
  }
1604
1566
  else {
1605
- const location = res.headers.get("location");
1606
- if (!location) {
1607
- redirectError = new Error("media download redirect missing location");
1567
+ try {
1568
+ nextUrl = new URL(location, currentUrl);
1608
1569
  }
1609
- else {
1610
- try {
1611
- nextUrl = new URL(location, currentUrl);
1612
- }
1613
- catch (error) {
1614
- redirectError = new Error("media download redirect is invalid", { cause: error });
1615
- }
1616
- if (nextUrl &&
1617
- ((nextUrl.protocol !== "http:" && nextUrl.protocol !== "https:") ||
1618
- nextUrl.username !== "" ||
1619
- nextUrl.password !== "")) {
1620
- redirectError = new Error("media download redirect is invalid");
1621
- }
1622
- if (nextUrl && nextUrl.origin !== trustedOrigin) {
1623
- redirectError = new Error("media download redirect crossed origin");
1624
- }
1570
+ catch (error) {
1571
+ redirectError = new Error("media download redirect is invalid", { cause: error });
1572
+ }
1573
+ if (nextUrl &&
1574
+ ((nextUrl.protocol !== "http:" && nextUrl.protocol !== "https:") ||
1575
+ nextUrl.username !== "" ||
1576
+ nextUrl.password !== "")) {
1577
+ redirectError = new Error("media download redirect is invalid");
1578
+ }
1579
+ if (nextUrl && nextUrl.origin !== trustedOrigin) {
1580
+ redirectError = new Error("media download redirect crossed origin");
1625
1581
  }
1626
1582
  }
1627
- await cancelResponseBody(res, "media redirect", redirectError);
1583
+ cancelResponseBody(res);
1628
1584
  if (redirectError)
1629
1585
  throw redirectError;
1630
1586
  currentUrl = nextUrl;
@@ -1,9 +1,7 @@
1
1
  /** Maximum plaintext media body accepted by the non-streaming SDK surface. */
2
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;
3
+ /** Matrix IDs and state-event strings are limited by UTF-8 bytes, not JS characters. */
4
+ export declare const MAX_MATRIX_IDENTIFIER_BYTES = 255;
7
5
  /**
8
6
  * Validates a new/current Matrix identity and binds its server name exactly to
9
7
  * the caller's independently trusted Matrix topology. The Matrix server name
@@ -11,6 +9,8 @@ export declare const MAX_MATRIX_IDENTIFIER_LENGTH = 512;
11
9
  * backend hostname that differs from the user-visible Matrix server name.
12
10
  */
13
11
  export declare function validateCanonicalMatrixUserId(value: unknown, serverName: unknown): string;
12
+ /** Validates a Matrix user ID without binding it to a particular homeserver. */
13
+ export declare function validateMatrixUserId(value: unknown): string;
14
14
  export declare function validateMatrixDeviceId(value: unknown): string;
15
15
  export declare function validateMatrixRoomId(value: unknown, name?: string): string;
16
16
  export declare function validateMatrixEventId(value: unknown, name?: string): string;
@@ -1,15 +1,13 @@
1
1
  /** Maximum plaintext media body accepted by the non-streaming SDK surface. */
2
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;
3
+ /** Matrix IDs and state-event strings are limited by UTF-8 bytes, not JS characters. */
4
+ export const MAX_MATRIX_IDENTIFIER_BYTES = 255;
7
5
  const MATRIX_SERVER_NAME_PATTERN = "(?:\\[[0-9A-Fa-f:.]+\\]|[A-Za-z0-9.-]+)(?::\\d{1,5})?";
8
6
  const MATRIX_SERVER_NAME_REGEXP = new RegExp(`^${MATRIX_SERVER_NAME_PATTERN}$`);
9
7
  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}$/;
8
+ const MATRIX_DEVICE_ID_PATTERN = /^[A-Za-z0-9._~-]+$/;
11
9
  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}$/;
10
+ const MATRIX_EVENT_ID_PATTERN = /^\$[A-Za-z0-9._~:/+\-]+$/;
13
11
  /**
14
12
  * Validates a new/current Matrix identity and binds its server name exactly to
15
13
  * the caller's independently trusted Matrix topology. The Matrix server name
@@ -17,28 +15,36 @@ const MATRIX_EVENT_ID_PATTERN = /^\$[A-Za-z0-9._~:/+\-]{1,511}$/;
17
15
  * backend hostname that differs from the user-visible Matrix server name.
18
16
  */
19
17
  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
- }
18
+ const userId = validateMatrixUserId(value);
24
19
  if (typeof serverName !== "string" || !MATRIX_SERVER_NAME_REGEXP.test(serverName)) {
25
20
  throw new Error("invalid Matrix server name");
26
21
  }
27
- const match = value.match(MATRIX_USER_ID_PATTERN);
22
+ const match = userId.match(MATRIX_USER_ID_PATTERN);
28
23
  if (!match || match[2].toLowerCase() !== serverName.toLowerCase()) {
29
24
  throw new Error("invalid Matrix user ID for this homeserver");
30
25
  }
26
+ return userId;
27
+ }
28
+ /** Validates a Matrix user ID without binding it to a particular homeserver. */
29
+ export function validateMatrixUserId(value) {
30
+ if (typeof value !== "string" ||
31
+ new TextEncoder().encode(value).byteLength > MAX_MATRIX_IDENTIFIER_BYTES) {
32
+ throw new Error("invalid Matrix user ID");
33
+ }
34
+ if (!MATRIX_USER_ID_PATTERN.test(value)) {
35
+ throw new Error("invalid Matrix user ID");
36
+ }
31
37
  return value;
32
38
  }
33
39
  export function validateMatrixDeviceId(value) {
34
- if (typeof value !== "string" || value.length > MAX_MATRIX_DEVICE_ID_LENGTH || !MATRIX_DEVICE_ID_PATTERN.test(value)) {
40
+ if (typeof value !== "string" || !MATRIX_DEVICE_ID_PATTERN.test(value)) {
35
41
  throw new Error("invalid Matrix device ID");
36
42
  }
37
43
  return value;
38
44
  }
39
45
  export function validateMatrixRoomId(value, name = "room ID") {
40
46
  if (typeof value !== "string" ||
41
- value.length > MAX_MATRIX_IDENTIFIER_LENGTH ||
47
+ new TextEncoder().encode(value).byteLength > MAX_MATRIX_IDENTIFIER_BYTES ||
42
48
  !MATRIX_ROOM_ID_PATTERN.test(value)) {
43
49
  throw new Error(`invalid Matrix ${name}`);
44
50
  }
@@ -46,7 +52,7 @@ export function validateMatrixRoomId(value, name = "room ID") {
46
52
  }
47
53
  export function validateMatrixEventId(value, name = "event ID") {
48
54
  if (typeof value !== "string" ||
49
- value.length > MAX_MATRIX_IDENTIFIER_LENGTH ||
55
+ new TextEncoder().encode(value).byteLength > MAX_MATRIX_IDENTIFIER_BYTES ||
50
56
  !MATRIX_EVENT_ID_PATTERN.test(value)) {
51
57
  throw new Error(`invalid Matrix ${name}`);
52
58
  }
@@ -23,8 +23,8 @@ export interface ResponseBodyOptions {
23
23
  * outcome; the late operation has no second result channel.
24
24
  */
25
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>;
26
+ /** Start discarding a rejected response without waiting for provider cleanup. */
27
+ export declare function cancelResponseBody(response: Response): void;
28
28
  export declare function readResponseBody(response: Response, signal?: AbortSignal, options?: ResponseBodyOptions): Promise<ResponseBody>;
29
29
  /** Read successful product media with the SDK's single plaintext-size ceiling. */
30
30
  export declare function readMediaResponseBody(response: Response, maxBytes: number, signal?: AbortSignal, options?: ResponseBodyOptions): Promise<ResponseBody>;