@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 +7 -10
- package/dist/TeleCryptIOStorage.d.ts +1 -1
- package/dist/TeleCryptIOStorage.js +39 -83
- package/dist/core/constants.d.ts +4 -4
- package/dist/core/constants.js +20 -14
- package/dist/core/http.d.ts +2 -2
- package/dist/core/http.js +24 -95
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +1 -0
- package/dist/core/oidc.d.ts +4 -6
- package/dist/core/oidc.js +111 -338
- package/dist/core/operations.js +82 -184
- package/package.json +2 -4
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
|
|
16
|
-
GitHub
|
|
17
|
-
|
|
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
|
|
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,
|
|
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
|
|
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>(
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
395
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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 (
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
1354
|
+
await this.cleanupCreatedRoom(tree.id);
|
|
1394
1355
|
}
|
|
1395
1356
|
catch (cleanupError) {
|
|
1396
1357
|
cleanupFailures.push(cleanupError);
|
|
1397
1358
|
}
|
|
1398
|
-
if (
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
1602
|
-
|
|
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
|
-
|
|
1606
|
-
|
|
1607
|
-
redirectError = new Error("media download redirect missing location");
|
|
1567
|
+
try {
|
|
1568
|
+
nextUrl = new URL(location, currentUrl);
|
|
1608
1569
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
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
|
-
|
|
1583
|
+
cancelResponseBody(res);
|
|
1628
1584
|
if (redirectError)
|
|
1629
1585
|
throw redirectError;
|
|
1630
1586
|
currentUrl = nextUrl;
|
package/dist/core/constants.d.ts
CHANGED
|
@@ -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
|
|
4
|
-
export declare const
|
|
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;
|
package/dist/core/constants.js
CHANGED
|
@@ -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
|
|
4
|
-
export const
|
|
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._~-]
|
|
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._~:/+\-]
|
|
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
|
-
|
|
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 =
|
|
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" ||
|
|
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.
|
|
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.
|
|
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
|
}
|
package/dist/core/http.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
27
|
-
export declare function cancelResponseBody(response: Response
|
|
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>;
|