@telecrypt-io/storage 0.5.26 → 0.5.28
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 +5 -1
- package/dist/TeleCryptIOStorage.d.ts +0 -6
- package/dist/TeleCryptIOStorage.js +0 -28
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/oidc.js +50 -7
- package/dist/core/operations.js +92 -111
- package/dist/core/validation.d.ts +2 -1
- package/dist/core/validation.js +8 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
End-to-end encrypted file storage and sharing, built on Matrix.
|
|
6
6
|
|
|
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
|
|
9
|
+
and development workflow.
|
|
10
|
+
|
|
7
11
|
Files are encrypted on the client before upload. The server stores only opaque ciphertext and
|
|
8
12
|
never holds the decryption keys. Shared vaults let multiple people add and read files, and a
|
|
9
13
|
Recovery Key restores your files on a new device — even if you lose the original.
|
|
@@ -15,7 +19,7 @@ is maintained in its own repository.
|
|
|
15
19
|
## Install
|
|
16
20
|
|
|
17
21
|
```bash
|
|
18
|
-
npm install @telecrypt-io/storage@0.5.
|
|
22
|
+
npm install @telecrypt-io/storage@0.5.28
|
|
19
23
|
```
|
|
20
24
|
|
|
21
25
|
This gives you the `TeleCryptIOStorage` library and its browser-safe `core` API.
|
|
@@ -285,12 +285,6 @@ export declare class TeleCryptIOStorage {
|
|
|
285
285
|
role: string;
|
|
286
286
|
membership: string;
|
|
287
287
|
}[]>;
|
|
288
|
-
/**
|
|
289
|
-
* Returns the homeserver's complete joined-room inventory. Local sync room
|
|
290
|
-
* lists can be truncated by initialSyncLimit and are not an authoritative
|
|
291
|
-
* basis for deletion safety.
|
|
292
|
-
*/
|
|
293
|
-
listJoinedRoomIds(options?: MatrixRequestOptions): Promise<string[]>;
|
|
294
288
|
/** Reads one room membership from the homeserver, not the lagging local sync store. */
|
|
295
289
|
getRoomMembership(roomId: string, userId?: string | null, options?: MatrixRequestOptions): Promise<string | null>;
|
|
296
290
|
uploadFile(tree: TreeSpace, name: string, data: ArrayBuffer, mimetype: string, signal?: AbortSignal): Promise<string>;
|
|
@@ -375,21 +375,6 @@ function parseMatrixPowerLevels(value) {
|
|
|
375
375
|
}
|
|
376
376
|
return value;
|
|
377
377
|
}
|
|
378
|
-
function parseJoinedRoomsResponse(value) {
|
|
379
|
-
if (!isRecord(value) ||
|
|
380
|
-
!Array.isArray(value.joined_rooms) ||
|
|
381
|
-
value.joined_rooms.length > MAX_MATRIX_JOINED_ROOMS) {
|
|
382
|
-
throw new Error("invalid Matrix joined-room response");
|
|
383
|
-
}
|
|
384
|
-
return value.joined_rooms.map((roomId) => {
|
|
385
|
-
try {
|
|
386
|
-
return validateMatrixRoomId(roomId, "joined-room response");
|
|
387
|
-
}
|
|
388
|
-
catch {
|
|
389
|
-
throw new Error("invalid Matrix joined-room response");
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
378
|
function isGoneRoomError(error) {
|
|
394
379
|
return (error instanceof MatrixError &&
|
|
395
380
|
(error.errcode === "M_NOT_FOUND" || error.errcode === "M_UNKNOWN"));
|
|
@@ -1435,19 +1420,6 @@ export class TeleCryptIOStorage {
|
|
|
1435
1420
|
role: roleFor(e.state_key),
|
|
1436
1421
|
}));
|
|
1437
1422
|
}
|
|
1438
|
-
/**
|
|
1439
|
-
* Returns the homeserver's complete joined-room inventory. Local sync room
|
|
1440
|
-
* lists can be truncated by initialSyncLimit and are not an authoritative
|
|
1441
|
-
* basis for deletion safety.
|
|
1442
|
-
*/
|
|
1443
|
-
async listJoinedRoomIds(options) {
|
|
1444
|
-
const body = await requireMatrixHttpTransport(this.client).authedRequest(Method.Get, "/joined_rooms", undefined, undefined, {
|
|
1445
|
-
prefix: ClientPrefix.V3,
|
|
1446
|
-
localTimeoutMs: matrixRequestTimeout(options),
|
|
1447
|
-
abortSignal: matrixRequestSignal(options),
|
|
1448
|
-
});
|
|
1449
|
-
return parseJoinedRoomsResponse(body);
|
|
1450
|
-
}
|
|
1451
1423
|
/** Reads one room membership from the homeserver, not the lagging local sync store. */
|
|
1452
1424
|
async getRoomMembership(roomId, userId = this.client.getUserId(), options) {
|
|
1453
1425
|
if (!userId)
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED
package/dist/core/oidc.js
CHANGED
|
@@ -35,6 +35,9 @@ const MAX_OIDC_TOKEN_LENGTH = 8192;
|
|
|
35
35
|
const MAX_OIDC_SCOPE_LENGTH = 4096;
|
|
36
36
|
const MAX_OIDC_CONTACTS = 16;
|
|
37
37
|
const MAX_OIDC_REDIRECT_URIS = 8;
|
|
38
|
+
const LEGACY_MATRIX_SCOPE_PREFIX = "urn:matrix:client:";
|
|
39
|
+
const MSC2967_MATRIX_SCOPE_PREFIX = "urn:matrix:org.matrix.msc2967.client:";
|
|
40
|
+
const MATRIX_SCOPE_PREFIXES = [LEGACY_MATRIX_SCOPE_PREFIX, MSC2967_MATRIX_SCOPE_PREFIX];
|
|
38
41
|
// MAS advertises a 20-minute device-code lifetime. Keep that provider window
|
|
39
42
|
// as the hard upper bound while retaining a finite polling budget.
|
|
40
43
|
const MAX_DEVICE_EXPIRES_IN_SECONDS = 20 * 60;
|
|
@@ -791,7 +794,9 @@ export async function waitForDeviceCodeLogin(authMetadata, clientId, session, si
|
|
|
791
794
|
}, Math.min(OIDC_REQUEST_TIMEOUT_MS, remaining), signal);
|
|
792
795
|
if (response.ok && isValidDeviceAccessTokenResponse(body)) {
|
|
793
796
|
const tokenResponse = validateDeviceAccessTokenResponse(body);
|
|
794
|
-
if (!requestedDeviceId ||
|
|
797
|
+
if (!requestedDeviceId ||
|
|
798
|
+
typeof tokenResponse.scope !== "string" ||
|
|
799
|
+
!scopeMatchesDevice(tokenResponse.scope, requestedDeviceId)) {
|
|
795
800
|
throw new OidcValidationError("OIDC device authorization returned an unexpected granted scope");
|
|
796
801
|
}
|
|
797
802
|
ensureOidcNotCancelled(signal, "device authorization");
|
|
@@ -940,9 +945,8 @@ export async function completeAuthorizationCodeFlow(code, state, signal) {
|
|
|
940
945
|
validateBearerTokenResponse(body);
|
|
941
946
|
validateTokenBounds(body, "authorization code exchange");
|
|
942
947
|
const normalized = normalizeBearerTokenResponseTokenType(body);
|
|
943
|
-
const expectedScope = generateScope(context.deviceId);
|
|
944
948
|
const grantedScope = normalized.scope;
|
|
945
|
-
if (typeof grantedScope !== "string" || grantedScope
|
|
949
|
+
if (typeof grantedScope !== "string" || !scopeMatchesDevice(grantedScope, context.deviceId)) {
|
|
946
950
|
throw new StorageError("OIDC authorization code exchange returned an unexpected granted scope");
|
|
947
951
|
}
|
|
948
952
|
const tokenResponse = {
|
|
@@ -975,12 +979,52 @@ export function extractDeviceIdFromScope(scope) {
|
|
|
975
979
|
return null;
|
|
976
980
|
}
|
|
977
981
|
const tokens = scope.split(/\s+/);
|
|
978
|
-
const deviceTokens = tokens.filter((token) => token.startsWith(
|
|
982
|
+
const deviceTokens = tokens.filter((token) => MATRIX_SCOPE_PREFIXES.some((prefix) => token.startsWith(`${prefix}device:`)));
|
|
979
983
|
if (deviceTokens.length !== 1)
|
|
980
984
|
return null;
|
|
981
|
-
const
|
|
985
|
+
const deviceToken = deviceTokens[0];
|
|
986
|
+
const prefix = MATRIX_SCOPE_PREFIXES.find((candidate) => deviceToken.startsWith(`${candidate}device:`));
|
|
987
|
+
if (!prefix)
|
|
988
|
+
return null;
|
|
989
|
+
const deviceId = deviceToken.slice(`${prefix}device:`.length);
|
|
982
990
|
return DEVICE_ID_PATTERN.test(deviceId) ? deviceId : null;
|
|
983
991
|
}
|
|
992
|
+
/**
|
|
993
|
+
* Accepts the legacy Matrix OAuth scope and the current MSC2967 namespace
|
|
994
|
+
* returned by MAS. The provider may add the standard `openid` scope, but the
|
|
995
|
+
* API and device grants must remain one exact namespace pair bound to the
|
|
996
|
+
* device that this client requested.
|
|
997
|
+
*/
|
|
998
|
+
function scopeMatchesDevice(scope, expectedDeviceId) {
|
|
999
|
+
if (typeof scope !== "string" ||
|
|
1000
|
+
typeof expectedDeviceId !== "string" ||
|
|
1001
|
+
!DEVICE_ID_PATTERN.test(expectedDeviceId) ||
|
|
1002
|
+
scope.length === 0 ||
|
|
1003
|
+
scope.length > MAX_OIDC_SCOPE_LENGTH ||
|
|
1004
|
+
/[\u0000-\u001f\u007f]/.test(scope) ||
|
|
1005
|
+
scope.trim() !== scope ||
|
|
1006
|
+
/\s{2,}/.test(scope)) {
|
|
1007
|
+
return false;
|
|
1008
|
+
}
|
|
1009
|
+
const tokens = scope.split(/\s+/);
|
|
1010
|
+
if (new Set(tokens).size !== tokens.length)
|
|
1011
|
+
return false;
|
|
1012
|
+
return MATRIX_SCOPE_PREFIXES.some((prefix) => {
|
|
1013
|
+
const expected = new Set([
|
|
1014
|
+
`${prefix}api:*`,
|
|
1015
|
+
`${prefix}device:${expectedDeviceId}`,
|
|
1016
|
+
"openid",
|
|
1017
|
+
]);
|
|
1018
|
+
const required = new Set([
|
|
1019
|
+
`${prefix}api:*`,
|
|
1020
|
+
`${prefix}device:${expectedDeviceId}`,
|
|
1021
|
+
]);
|
|
1022
|
+
return (tokens.length >= required.size &&
|
|
1023
|
+
tokens.length <= expected.size &&
|
|
1024
|
+
tokens.every((token) => expected.has(token)) &&
|
|
1025
|
+
[...required].every((token) => tokens.includes(token)));
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
984
1028
|
function validateMatrixUserId(userId, serverName) {
|
|
985
1029
|
try {
|
|
986
1030
|
return validateCanonicalMatrixUserId(userId, serverName);
|
|
@@ -1120,8 +1164,7 @@ async function refreshOidcToken(tokenEndpoint, clientId, refreshToken, expectedD
|
|
|
1120
1164
|
catch {
|
|
1121
1165
|
throw new StorageError("OIDC token refresh returned an invalid scope");
|
|
1122
1166
|
}
|
|
1123
|
-
if (
|
|
1124
|
-
grantedScope !== generateScope(expectedDeviceId)) {
|
|
1167
|
+
if (!scopeMatchesDevice(grantedScope, expectedDeviceId)) {
|
|
1125
1168
|
throw new StorageError("OIDC token refresh returned an unexpected granted scope");
|
|
1126
1169
|
}
|
|
1127
1170
|
}
|
package/dist/core/operations.js
CHANGED
|
@@ -32,7 +32,6 @@ const RATE_LIMIT_DEFAULT_DELAY_MS = 15_000;
|
|
|
32
32
|
const RATE_LIMIT_MAX_DELAY_MS = 30_000;
|
|
33
33
|
const RATE_LIMIT_MAX_TOTAL_DELAY_MS = 90_000;
|
|
34
34
|
const MAX_DELETION_ROOMS = 4096;
|
|
35
|
-
const DELETION_REFRESH_CONCURRENCY = 8;
|
|
36
35
|
const MAX_DELETION_DEPTH = 128;
|
|
37
36
|
const MAX_FILE_VERSION_CHAIN = 128;
|
|
38
37
|
const MAX_LIST_ITEMS = 10000;
|
|
@@ -257,7 +256,7 @@ function assertTreeEmptyForDeletion(storage, tree, spaces) {
|
|
|
257
256
|
if (files.length > 0)
|
|
258
257
|
throw new NonEmptyTreeError(tree.id);
|
|
259
258
|
}
|
|
260
|
-
async function refreshDeletionRooms(storage, signal) {
|
|
259
|
+
async function refreshDeletionRooms(storage, rootId, signal) {
|
|
261
260
|
const client = storage.getClient();
|
|
262
261
|
const http = client.http;
|
|
263
262
|
// A real MatrixClient always exposes its authenticated HTTP transport. A
|
|
@@ -265,48 +264,31 @@ async function refreshDeletionRooms(storage, signal) {
|
|
|
265
264
|
if (typeof http?.authedRequest !== "function") {
|
|
266
265
|
throw new StorageError("delete graph refresh is unavailable");
|
|
267
266
|
}
|
|
268
|
-
|
|
269
|
-
if (
|
|
270
|
-
throw new StorageError("
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
for (const
|
|
278
|
-
if (!
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
267
|
+
await storage.refreshRoomState(rootId, { signal });
|
|
268
|
+
if (signal?.aborted)
|
|
269
|
+
throw new StorageError("operation cancelled");
|
|
270
|
+
// The refreshed root owns its parent relation. Verify its one supported
|
|
271
|
+
// external parent directly; unrelated local rooms are outside this deletion.
|
|
272
|
+
const parentEvents = readRelationEvents(client, rootId, EventType.SpaceParent);
|
|
273
|
+
if (parentEvents === null)
|
|
274
|
+
throw new StorageError("delete graph is unsafe");
|
|
275
|
+
const parentIds = new Set();
|
|
276
|
+
for (const event of parentEvents) {
|
|
277
|
+
if (!isActiveRelationEvent(event))
|
|
278
|
+
continue;
|
|
279
|
+
const parentId = relationStateKey(event);
|
|
280
|
+
if (!parentId)
|
|
281
|
+
throw new StorageError("delete graph is unsafe");
|
|
282
|
+
if (parentId !== rootId)
|
|
283
|
+
parentIds.add(parentId);
|
|
284
284
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
while (!failed && !signal?.aborted) {
|
|
291
|
-
const roomId = roomIdsToRefresh[nextRoom++];
|
|
292
|
-
if (roomId === undefined)
|
|
293
|
-
return;
|
|
294
|
-
try {
|
|
295
|
-
await storage.refreshRoomState(roomId, { signal });
|
|
296
|
-
}
|
|
297
|
-
catch (error) {
|
|
298
|
-
failed = true;
|
|
299
|
-
firstError = error;
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
const workerCount = Math.min(DELETION_REFRESH_CONCURRENCY, roomIdsToRefresh.length);
|
|
305
|
-
await Promise.all(Array.from({ length: workerCount }, () => refreshWorker()));
|
|
285
|
+
if (parentIds.size > 1)
|
|
286
|
+
throw new StorageError("delete graph is unsafe");
|
|
287
|
+
const parentId = parentIds.values().next().value;
|
|
288
|
+
if (parentId)
|
|
289
|
+
await storage.refreshRoomState(parentId, { signal });
|
|
306
290
|
if (signal?.aborted)
|
|
307
291
|
throw new StorageError("operation cancelled");
|
|
308
|
-
if (failed)
|
|
309
|
-
throw firstError;
|
|
310
292
|
}
|
|
311
293
|
async function refreshTreeSpaces(storage, spaces, signal) {
|
|
312
294
|
for (const space of spaces) {
|
|
@@ -420,31 +402,11 @@ function validateDeletionGraph(client, root, ids, deletedRooms) {
|
|
|
420
402
|
if (id !== root.id || externalParents.includes(parentId)) {
|
|
421
403
|
throw new StorageError("delete graph is unsafe");
|
|
422
404
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
const rooms = client.getRooms();
|
|
428
|
-
if (rooms.length > MAX_DELETION_ROOMS)
|
|
429
|
-
throw new StorageError("delete graph is too large");
|
|
430
|
-
for (const room of rooms) {
|
|
431
|
-
const parentId = room.roomId;
|
|
432
|
-
const childEvents = readRelationEvents(client, parentId, EventType.SpaceChild);
|
|
433
|
-
if (childEvents === null)
|
|
434
|
-
throw new StorageError("delete graph is unsafe");
|
|
435
|
-
for (const event of childEvents) {
|
|
436
|
-
if (!isActiveRelationEvent(event))
|
|
437
|
-
continue;
|
|
438
|
-
const childId = relationStateKey(event);
|
|
439
|
-
if (!childId)
|
|
440
|
-
throw new StorageError("delete graph is unsafe");
|
|
441
|
-
if (ids.has(childId) && !ids.has(parentId)) {
|
|
442
|
-
if (childId === root.id) {
|
|
443
|
-
if (!externalParents.includes(parentId))
|
|
444
|
-
externalParents.push(parentId);
|
|
445
|
-
continue;
|
|
405
|
+
const childLink = readRelationEvents(client, parentId, EventType.SpaceChild, id);
|
|
406
|
+
if (!childLink?.some((candidate) => isActiveRelationEvent(candidate) && relationStateKey(candidate, id) === id)) {
|
|
407
|
+
throw new StorageError("delete graph is unsafe");
|
|
446
408
|
}
|
|
447
|
-
|
|
409
|
+
externalParents.push(parentId);
|
|
448
410
|
}
|
|
449
411
|
}
|
|
450
412
|
}
|
|
@@ -524,60 +486,78 @@ function isGoneError(error) {
|
|
|
524
486
|
(error.errcode === "M_NOT_FOUND" || error.errcode === "M_UNKNOWN"));
|
|
525
487
|
}
|
|
526
488
|
async function deleteRoomDeterministically(storage, roomId, signal) {
|
|
527
|
-
const
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
if (member.userId
|
|
542
|
-
(membership
|
|
543
|
-
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (isGoneError(error))
|
|
489
|
+
const completedRoomIds = [];
|
|
490
|
+
const markRoomMutationComplete = () => {
|
|
491
|
+
if (!completedRoomIds.includes(roomId))
|
|
492
|
+
completedRoomIds.push(roomId);
|
|
493
|
+
};
|
|
494
|
+
try {
|
|
495
|
+
const client = storage.getClient();
|
|
496
|
+
const tree = storage.getTree(roomId);
|
|
497
|
+
if (!tree)
|
|
498
|
+
throw new StorageError("delete graph is unsafe");
|
|
499
|
+
const self = client.getUserId();
|
|
500
|
+
if (!self)
|
|
501
|
+
throw new StorageError("delete graph is unsafe");
|
|
502
|
+
const members = await storage.listMembers(tree, { signal });
|
|
503
|
+
if (members.some((member) => member.userId !== self && member.role === "owner" &&
|
|
504
|
+
(member.membership === "join" || member.membership === "invite" || member.membership === "knock"))) {
|
|
505
|
+
throw new StorageError("delete will not kick another room owner");
|
|
506
|
+
}
|
|
507
|
+
for (const member of members) {
|
|
508
|
+
const membership = member.membership;
|
|
509
|
+
if (member.userId === self ||
|
|
510
|
+
(membership !== "join" && membership !== "invite" && membership !== "knock")) {
|
|
550
511
|
continue;
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(client, () => client.kick(roomId, member.userId, "Room deleted"), signal), signal);
|
|
515
|
+
markRoomMutationComplete();
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
if (isGoneError(error))
|
|
556
519
|
continue;
|
|
520
|
+
if (error instanceof MatrixError && error.errcode === "M_FORBIDDEN") {
|
|
521
|
+
const currentMembership = await storage.getRoomMembership(roomId, member.userId, { signal });
|
|
522
|
+
if (currentMembership === "leave" ||
|
|
523
|
+
currentMembership === "ban" ||
|
|
524
|
+
!currentMembership) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
557
527
|
}
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const ownMembership = await storage.getRoomMembership(roomId, undefined, { signal });
|
|
532
|
+
if (ownMembership === "join" || ownMembership === "invite" || ownMembership === "knock") {
|
|
533
|
+
try {
|
|
534
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(client, () => client.leave(roomId), signal), signal);
|
|
535
|
+
markRoomMutationComplete();
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
if (!isGoneError(error))
|
|
539
|
+
throw error;
|
|
558
540
|
}
|
|
559
|
-
throw error;
|
|
560
541
|
}
|
|
561
|
-
}
|
|
562
|
-
const ownMembership = await storage.getRoomMembership(roomId, undefined, { signal });
|
|
563
|
-
if (ownMembership === "join" || ownMembership === "invite" || ownMembership === "knock") {
|
|
564
542
|
try {
|
|
565
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(client, () => client.
|
|
543
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(client, () => client.forget(roomId), signal), signal);
|
|
544
|
+
markRoomMutationComplete();
|
|
566
545
|
}
|
|
567
546
|
catch (error) {
|
|
568
547
|
if (!isGoneError(error))
|
|
569
548
|
throw error;
|
|
570
549
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(client, () => client.forget(roomId), signal), signal);
|
|
550
|
+
removeRoomFromLocalStore(client, roomId);
|
|
551
|
+
markTreeDeleted(client, roomId);
|
|
574
552
|
}
|
|
575
553
|
catch (error) {
|
|
576
|
-
if (
|
|
554
|
+
if (error instanceof MutationOutcomeUnknownError || error instanceof MutationPartialError)
|
|
577
555
|
throw error;
|
|
556
|
+
if (completedRoomIds.length > 0) {
|
|
557
|
+
throw new MutationPartialError("delete", completedRoomIds, "room cleanup stopped");
|
|
558
|
+
}
|
|
559
|
+
throw error;
|
|
578
560
|
}
|
|
579
|
-
removeRoomFromLocalStore(client, roomId);
|
|
580
|
-
markTreeDeleted(client, roomId);
|
|
581
561
|
}
|
|
582
562
|
/**
|
|
583
563
|
* The SDK normally evicts a room as part of forget(). Keep the local-store
|
|
@@ -1134,10 +1114,11 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1134
1114
|
const removedRooms = getDeletedTreeIds(client);
|
|
1135
1115
|
let graph;
|
|
1136
1116
|
try {
|
|
1137
|
-
// Re-read
|
|
1138
|
-
//
|
|
1139
|
-
//
|
|
1140
|
-
|
|
1117
|
+
// Re-read the candidate graph and any external parents it identifies
|
|
1118
|
+
// before taking the immutable graph snapshot. Unrelated rooms are not
|
|
1119
|
+
// part of this operation and must not block deletion merely because
|
|
1120
|
+
// their membership is not joined.
|
|
1121
|
+
await refreshDeletionRooms(storage, tree.id, operation.signal);
|
|
1141
1122
|
const spaces = snapshotTreeSpaces(tree, "delete graph is too large");
|
|
1142
1123
|
const activeSpaces = spaces.filter((space) => !removedRooms.has(space.id));
|
|
1143
1124
|
const ids = new Set(activeSpaces.map((space) => space.id));
|
|
@@ -1155,7 +1136,7 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1155
1136
|
error instanceof MutationPartialError ||
|
|
1156
1137
|
error instanceof NonEmptyTreeError)
|
|
1157
1138
|
throw error;
|
|
1158
|
-
if (error instanceof StorageError
|
|
1139
|
+
if (error instanceof StorageError)
|
|
1159
1140
|
throw error;
|
|
1160
1141
|
throw new StorageError("delete failed");
|
|
1161
1142
|
}
|
|
@@ -1172,7 +1153,7 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1172
1153
|
if (error instanceof StorageError && error.message === "delete graph unlink cleanup is incomplete") {
|
|
1173
1154
|
throw error;
|
|
1174
1155
|
}
|
|
1175
|
-
if (error instanceof StorageError
|
|
1156
|
+
if (error instanceof StorageError)
|
|
1176
1157
|
throw error;
|
|
1177
1158
|
throw new StorageError("delete failed");
|
|
1178
1159
|
}
|
|
@@ -1193,7 +1174,7 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1193
1174
|
throw error;
|
|
1194
1175
|
if (error instanceof MutationPartialError)
|
|
1195
1176
|
throw error;
|
|
1196
|
-
if (error instanceof StorageError
|
|
1177
|
+
if (error instanceof StorageError)
|
|
1197
1178
|
throw error;
|
|
1198
1179
|
throw new StorageError("delete failed");
|
|
1199
1180
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Shared name contract used by SDK inputs and remote projections. */
|
|
2
2
|
export declare const MAX_NAME_LENGTH = 512;
|
|
3
|
+
export declare function isValidName(value: unknown): value is string;
|
|
3
4
|
export declare function validateName(value: unknown, label: string): asserts value is string;
|
package/dist/core/validation.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Shared name contract used by SDK inputs and remote projections. */
|
|
2
2
|
export const MAX_NAME_LENGTH = 512;
|
|
3
|
+
export function isValidName(value) {
|
|
4
|
+
return (typeof value === "string" &&
|
|
5
|
+
value.length > 0 &&
|
|
6
|
+
value.length <= MAX_NAME_LENGTH &&
|
|
7
|
+
!/[\u0000-\u001f\u007f]/.test(value));
|
|
8
|
+
}
|
|
3
9
|
export function validateName(value, label) {
|
|
4
|
-
if (
|
|
5
|
-
value.length === 0 ||
|
|
6
|
-
value.length > MAX_NAME_LENGTH ||
|
|
7
|
-
/[\u0000-\u001f\u007f]/.test(value)) {
|
|
10
|
+
if (!isValidName(value)) {
|
|
8
11
|
throw new Error(`invalid ${label}`);
|
|
9
12
|
}
|
|
10
13
|
}
|