@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/dist/core/operations.js
CHANGED
|
@@ -17,7 +17,7 @@ import { Method } from "matrix-js-sdk/lib/http-api/method.js";
|
|
|
17
17
|
import { FileTooLargeError, MutationPartialError, MutationOutcomeUnknownError, NonEmptyTreeError, RoomCreationAmbiguousError, RoomCleanupIncompleteError, StorageError, } from "./errors.js";
|
|
18
18
|
import { ConditionTimeoutError, waitForCondition } from "./poll.js";
|
|
19
19
|
import { validateName } from "./validation.js";
|
|
20
|
-
import {
|
|
20
|
+
import { isFileDeleted, isTreeDeleted, markFileDeleted, markTreeDeleted, } from "../deletion-markers.js";
|
|
21
21
|
/**
|
|
22
22
|
* Production Synapse enforces the built-in rc_messages budget (per-account
|
|
23
23
|
* burst 10, refill 1/5s) on every room/state mutation the tree operations
|
|
@@ -100,9 +100,7 @@ function isRateLimited(error) {
|
|
|
100
100
|
function requiresMutationReconciliation(error) {
|
|
101
101
|
return (error instanceof MutationOutcomeUnknownError ||
|
|
102
102
|
error instanceof RoomCreationAmbiguousError ||
|
|
103
|
-
error instanceof RoomCleanupIncompleteError
|
|
104
|
-
(error instanceof Error &&
|
|
105
|
-
error.cleanupIncomplete === true));
|
|
103
|
+
error instanceof RoomCleanupIncompleteError);
|
|
106
104
|
}
|
|
107
105
|
async function withRateLimitRetry(operation, signal) {
|
|
108
106
|
for (;;) {
|
|
@@ -175,13 +173,9 @@ async function resolveTree(storage, treeId, signal) {
|
|
|
175
173
|
}
|
|
176
174
|
}
|
|
177
175
|
function isMarkedTreeDeleted(storage, treeId) {
|
|
178
|
-
if (typeof storage.getClient !== "function")
|
|
179
|
-
return false;
|
|
180
176
|
return isTreeDeleted(storage.getClient(), treeId);
|
|
181
177
|
}
|
|
182
178
|
function isMarkedFileDeleted(storage, treeId, fileId) {
|
|
183
|
-
if (typeof storage.getClient !== "function")
|
|
184
|
-
return false;
|
|
185
179
|
return isFileDeleted(storage.getClient(), treeId, fileId);
|
|
186
180
|
}
|
|
187
181
|
/** As `resolveTree`, but for a specific file within an already-resolved
|
|
@@ -210,9 +204,7 @@ async function resolveFile(storage, tree, fileId, signal) {
|
|
|
210
204
|
* room. This also prevents a folder/vault delete from silently deleting a
|
|
211
205
|
* nested shared tree.
|
|
212
206
|
*/
|
|
213
|
-
function assertTreeEmptyForDeletion(storage, tree
|
|
214
|
-
if (spaces.length !== 1)
|
|
215
|
-
throw new NonEmptyTreeError(tree.id);
|
|
207
|
+
function assertTreeEmptyForDeletion(storage, tree) {
|
|
216
208
|
let files;
|
|
217
209
|
try {
|
|
218
210
|
files = tree.listFiles().filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id));
|
|
@@ -222,40 +214,74 @@ function assertTreeEmptyForDeletion(storage, tree, spaces) {
|
|
|
222
214
|
}
|
|
223
215
|
if (files.length > 0)
|
|
224
216
|
throw new NonEmptyTreeError(tree.id);
|
|
217
|
+
const childEvents = readRelationEvents(storage.getClient(), tree.id, EventType.SpaceChild);
|
|
218
|
+
if (childEvents === null)
|
|
219
|
+
throw new StorageError("could not enumerate storage folders safely");
|
|
220
|
+
for (const event of childEvents) {
|
|
221
|
+
if (!isActiveRelationEvent(event))
|
|
222
|
+
continue;
|
|
223
|
+
const childId = relationStateKey(event);
|
|
224
|
+
if (!childId)
|
|
225
|
+
throw new StorageError("could not enumerate storage folders safely");
|
|
226
|
+
if (!isTreeDeleted(storage.getClient(), childId))
|
|
227
|
+
throw new NonEmptyTreeError(tree.id);
|
|
228
|
+
}
|
|
225
229
|
}
|
|
226
230
|
async function refreshDeletionRooms(storage, rootId, signal) {
|
|
227
231
|
const client = storage.getClient();
|
|
228
|
-
const http = client.http;
|
|
229
|
-
// A real MatrixClient always exposes its authenticated HTTP transport. A
|
|
230
|
-
// client without one cannot prove that its local relation graph is current.
|
|
231
|
-
if (typeof http?.authedRequest !== "function") {
|
|
232
|
-
throw new StorageError("delete graph refresh is unavailable");
|
|
233
|
-
}
|
|
234
232
|
await storage.refreshRoomState(rootId, { signal });
|
|
235
233
|
if (signal?.aborted)
|
|
236
234
|
throw new StorageError("operation cancelled");
|
|
237
|
-
// The refreshed root owns its parent relation. Verify its one supported
|
|
238
|
-
// external parent directly; unrelated local rooms are outside this deletion.
|
|
239
235
|
const parentEvents = readRelationEvents(client, rootId, EventType.SpaceParent);
|
|
240
236
|
if (parentEvents === null)
|
|
241
|
-
throw new StorageError("
|
|
237
|
+
throw new StorageError("could not enumerate storage parent safely");
|
|
242
238
|
const parentIds = new Set();
|
|
243
239
|
for (const event of parentEvents) {
|
|
244
240
|
if (!isActiveRelationEvent(event))
|
|
245
241
|
continue;
|
|
246
242
|
const parentId = relationStateKey(event);
|
|
247
243
|
if (!parentId)
|
|
248
|
-
throw new StorageError("
|
|
244
|
+
throw new StorageError("could not enumerate storage parent safely");
|
|
249
245
|
if (parentId !== rootId)
|
|
250
246
|
parentIds.add(parentId);
|
|
251
247
|
}
|
|
252
248
|
if (parentIds.size > 1)
|
|
253
|
-
throw new StorageError("
|
|
254
|
-
const
|
|
255
|
-
if (
|
|
256
|
-
await storage.refreshRoomState(
|
|
249
|
+
throw new StorageError("storage room has multiple parents");
|
|
250
|
+
const parents = [...parentIds];
|
|
251
|
+
if (parents[0]) {
|
|
252
|
+
await storage.refreshRoomState(parents[0], { signal });
|
|
253
|
+
const childEvents = readRelationEvents(client, parents[0], EventType.SpaceChild, rootId);
|
|
254
|
+
if (childEvents === null ||
|
|
255
|
+
!childEvents.some((event) => isActiveRelationEvent(event) && relationStateKey(event, rootId) === rootId)) {
|
|
256
|
+
throw new StorageError("storage parent relation is inconsistent");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
257
259
|
if (signal?.aborted)
|
|
258
260
|
throw new StorageError("operation cancelled");
|
|
261
|
+
return parents;
|
|
262
|
+
}
|
|
263
|
+
function snapshotTreeSpaces(root) {
|
|
264
|
+
const spaces = [];
|
|
265
|
+
const seen = new Set();
|
|
266
|
+
const pending = [root];
|
|
267
|
+
while (pending.length > 0) {
|
|
268
|
+
const tree = pending.pop();
|
|
269
|
+
if (!tree || seen.has(tree.id))
|
|
270
|
+
continue;
|
|
271
|
+
seen.add(tree.id);
|
|
272
|
+
spaces.push(tree);
|
|
273
|
+
let children;
|
|
274
|
+
try {
|
|
275
|
+
children = tree.getDirectories();
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (error instanceof StorageError)
|
|
279
|
+
throw error;
|
|
280
|
+
throw new StorageError("could not enumerate storage folders", { cause: error });
|
|
281
|
+
}
|
|
282
|
+
pending.push(...children);
|
|
283
|
+
}
|
|
284
|
+
return spaces;
|
|
259
285
|
}
|
|
260
286
|
async function refreshTreeSpaces(storage, spaces, signal) {
|
|
261
287
|
for (const space of spaces) {
|
|
@@ -286,126 +312,24 @@ function isActiveRelationEvent(event) {
|
|
|
286
312
|
function activeTreeDirectories(client, tree) {
|
|
287
313
|
const childEvents = readRelationEvents(client, tree.id, EventType.SpaceChild);
|
|
288
314
|
if (childEvents === null)
|
|
289
|
-
throw new StorageError("
|
|
315
|
+
throw new StorageError("storage folder state is unavailable");
|
|
290
316
|
const activeChildIds = new Set();
|
|
291
317
|
for (const event of childEvents) {
|
|
292
318
|
if (!isActiveRelationEvent(event))
|
|
293
319
|
continue;
|
|
294
320
|
const childId = relationStateKey(event);
|
|
295
321
|
if (!childId)
|
|
296
|
-
throw new StorageError("
|
|
322
|
+
throw new StorageError("storage folder state is inconsistent");
|
|
297
323
|
activeChildIds.add(childId);
|
|
298
324
|
}
|
|
299
325
|
const knownChildren = new Map(tree.getDirectories().map((child) => [child.id, child]));
|
|
300
326
|
return [...activeChildIds].map((childId) => {
|
|
301
327
|
const child = knownChildren.get(childId);
|
|
302
328
|
if (!child)
|
|
303
|
-
throw new StorageError("
|
|
329
|
+
throw new StorageError("storage folder state is inconsistent");
|
|
304
330
|
return child;
|
|
305
331
|
});
|
|
306
332
|
}
|
|
307
|
-
function snapshotTreeSpaces(root, activeRelationClient) {
|
|
308
|
-
const spaces = [];
|
|
309
|
-
const seen = new Set();
|
|
310
|
-
const pending = [root];
|
|
311
|
-
while (pending.length > 0) {
|
|
312
|
-
const tree = pending.pop();
|
|
313
|
-
if (!tree)
|
|
314
|
-
continue;
|
|
315
|
-
if (seen.has(tree.id))
|
|
316
|
-
continue;
|
|
317
|
-
seen.add(tree.id);
|
|
318
|
-
spaces.push(tree);
|
|
319
|
-
let children;
|
|
320
|
-
try {
|
|
321
|
-
children = activeRelationClient
|
|
322
|
-
? activeTreeDirectories(activeRelationClient, tree)
|
|
323
|
-
: tree.getDirectories();
|
|
324
|
-
}
|
|
325
|
-
catch (error) {
|
|
326
|
-
if (error instanceof StorageError)
|
|
327
|
-
throw error;
|
|
328
|
-
throw new StorageError("could not enumerate storage space descendants safely", { cause: error });
|
|
329
|
-
}
|
|
330
|
-
for (const child of children)
|
|
331
|
-
pending.push(child);
|
|
332
|
-
}
|
|
333
|
-
return spaces;
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Deletion is intentionally narrower than MSC3089TreeSpace.delete(): a local
|
|
337
|
-
* child graph is not proof that the rooms are exclusively owned by this tree.
|
|
338
|
-
* Validate every observed edge in both directions and reject any parent or
|
|
339
|
-
* child edge outside the exact deletion set before the first mutation.
|
|
340
|
-
*/
|
|
341
|
-
function validateDeletionGraph(client, root, ids, deletedRooms) {
|
|
342
|
-
const edges = new Set();
|
|
343
|
-
const externalParents = [];
|
|
344
|
-
const addEdge = (parentId, childId) => {
|
|
345
|
-
if (deletedRooms.has(childId))
|
|
346
|
-
return;
|
|
347
|
-
if (!ids.has(childId))
|
|
348
|
-
throw new StorageError("delete graph is unsafe");
|
|
349
|
-
edges.add(`${parentId}\u0000${childId}`);
|
|
350
|
-
};
|
|
351
|
-
for (const id of ids) {
|
|
352
|
-
const tree = client.unstableGetFileTreeSpace(id);
|
|
353
|
-
if (!tree)
|
|
354
|
-
throw new StorageError("delete graph is unsafe");
|
|
355
|
-
const directories = activeTreeDirectories(client, tree);
|
|
356
|
-
for (const child of directories)
|
|
357
|
-
addEdge(id, child.id);
|
|
358
|
-
const childEvents = readRelationEvents(client, id, EventType.SpaceChild);
|
|
359
|
-
if (childEvents === null)
|
|
360
|
-
throw new StorageError("delete graph is unsafe");
|
|
361
|
-
for (const event of childEvents) {
|
|
362
|
-
if (!isActiveRelationEvent(event))
|
|
363
|
-
continue;
|
|
364
|
-
const childId = relationStateKey(event);
|
|
365
|
-
if (!childId)
|
|
366
|
-
throw new StorageError("delete graph is unsafe");
|
|
367
|
-
addEdge(id, childId);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
for (const edge of edges) {
|
|
371
|
-
const separator = edge.indexOf("\u0000");
|
|
372
|
-
const parentId = edge.slice(0, separator);
|
|
373
|
-
const childId = edge.slice(separator + 1);
|
|
374
|
-
const childLink = readRelationEvents(client, parentId, EventType.SpaceChild, childId);
|
|
375
|
-
const parentLink = readRelationEvents(client, childId, EventType.SpaceParent, parentId);
|
|
376
|
-
if (!childLink?.some((event) => isActiveRelationEvent(event) && relationStateKey(event, childId) === childId) ||
|
|
377
|
-
!parentLink?.some((event) => isActiveRelationEvent(event) && relationStateKey(event, parentId) === parentId)) {
|
|
378
|
-
throw new StorageError("delete graph is unsafe");
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
for (const id of ids) {
|
|
382
|
-
const parentEvents = readRelationEvents(client, id, EventType.SpaceParent);
|
|
383
|
-
if (parentEvents === null)
|
|
384
|
-
throw new StorageError("delete graph is unsafe");
|
|
385
|
-
for (const event of parentEvents) {
|
|
386
|
-
if (!isActiveRelationEvent(event))
|
|
387
|
-
continue;
|
|
388
|
-
const parentId = relationStateKey(event);
|
|
389
|
-
if (!parentId)
|
|
390
|
-
throw new StorageError("delete graph is unsafe");
|
|
391
|
-
if (!ids.has(parentId)) {
|
|
392
|
-
if (id !== root.id || externalParents.includes(parentId)) {
|
|
393
|
-
throw new StorageError("delete graph is unsafe");
|
|
394
|
-
}
|
|
395
|
-
const childLink = readRelationEvents(client, parentId, EventType.SpaceChild, id);
|
|
396
|
-
if (!childLink?.some((candidate) => isActiveRelationEvent(candidate) && relationStateKey(candidate, id) === id)) {
|
|
397
|
-
throw new StorageError("delete graph is unsafe");
|
|
398
|
-
}
|
|
399
|
-
externalParents.push(parentId);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
if (!ids.has(root.id))
|
|
404
|
-
throw new StorageError("delete graph is unsafe");
|
|
405
|
-
if (externalParents.length > 1)
|
|
406
|
-
throw new StorageError("delete graph is unsafe");
|
|
407
|
-
return { externalParents };
|
|
408
|
-
}
|
|
409
333
|
async function unlinkExternalParents(storage, rootId, externalParents, signal) {
|
|
410
334
|
const client = storage.getClient();
|
|
411
335
|
let mutationAttempted = false;
|
|
@@ -417,10 +341,10 @@ async function unlinkExternalParents(storage, rootId, externalParents, signal) {
|
|
|
417
341
|
// is not used here because it can be delayed or rejected independently of
|
|
418
342
|
// the relation update.
|
|
419
343
|
mutationAttempted = true;
|
|
420
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(
|
|
344
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => client.sendStateEvent(parentId, EventType.SpaceChild, {}, rootId), signal), signal);
|
|
421
345
|
if (signal?.aborted)
|
|
422
346
|
throw new StorageError("operation cancelled");
|
|
423
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(
|
|
347
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => client.sendStateEvent(rootId, EventType.SpaceParent, {}, parentId), signal), signal);
|
|
424
348
|
await storage.refreshRoomState(parentId, { signal });
|
|
425
349
|
await storage.refreshRoomState(rootId, { signal });
|
|
426
350
|
const childLink = readRelationEvents(client, parentId, EventType.SpaceChild, rootId);
|
|
@@ -461,8 +385,8 @@ async function relinkExternalParents(storage, rootId, externalParents, signal) {
|
|
|
461
385
|
for (const parentId of externalParents) {
|
|
462
386
|
if (signal?.aborted)
|
|
463
387
|
throw new StorageError("operation cancelled");
|
|
464
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(
|
|
465
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(
|
|
388
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => client.sendStateEvent(parentId, EventType.SpaceChild, { via: [via] }, rootId), signal), signal);
|
|
389
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => client.sendStateEvent(rootId, EventType.SpaceParent, { via: [via] }, parentId), signal), signal);
|
|
466
390
|
await storage.refreshRoomState(parentId, { signal });
|
|
467
391
|
await storage.refreshRoomState(rootId, { signal });
|
|
468
392
|
const childLink = readRelationEvents(client, parentId, EventType.SpaceChild, rootId);
|
|
@@ -487,10 +411,10 @@ async function deleteRoomDeterministically(storage, roomId, signal) {
|
|
|
487
411
|
const client = storage.getClient();
|
|
488
412
|
const tree = storage.getTree(roomId);
|
|
489
413
|
if (!tree)
|
|
490
|
-
throw new StorageError("
|
|
414
|
+
throw new StorageError("storage room is unavailable for deletion");
|
|
491
415
|
const self = client.getUserId();
|
|
492
416
|
if (!self)
|
|
493
|
-
throw new StorageError("
|
|
417
|
+
throw new StorageError("Matrix user identity is unavailable");
|
|
494
418
|
const members = await storage.listMembers(tree, { signal });
|
|
495
419
|
if (members.some((member) => member.userId !== self && member.role === "owner" &&
|
|
496
420
|
(member.membership === "join" || member.membership === "invite" || member.membership === "knock"))) {
|
|
@@ -503,7 +427,7 @@ async function deleteRoomDeterministically(storage, roomId, signal) {
|
|
|
503
427
|
continue;
|
|
504
428
|
}
|
|
505
429
|
try {
|
|
506
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(
|
|
430
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => client.kick(roomId, member.userId, "Room deleted"), signal), signal);
|
|
507
431
|
markRoomMutationComplete();
|
|
508
432
|
}
|
|
509
433
|
catch (error) {
|
|
@@ -523,7 +447,7 @@ async function deleteRoomDeterministically(storage, roomId, signal) {
|
|
|
523
447
|
const ownMembership = await storage.getRoomMembership(roomId, undefined, { signal });
|
|
524
448
|
if (ownMembership === "join" || ownMembership === "invite" || ownMembership === "knock") {
|
|
525
449
|
try {
|
|
526
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(
|
|
450
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => client.leave(roomId), signal), signal);
|
|
527
451
|
markRoomMutationComplete();
|
|
528
452
|
}
|
|
529
453
|
catch (error) {
|
|
@@ -532,7 +456,7 @@ async function deleteRoomDeterministically(storage, roomId, signal) {
|
|
|
532
456
|
}
|
|
533
457
|
}
|
|
534
458
|
try {
|
|
535
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(
|
|
459
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => client.forget(roomId), signal), signal);
|
|
536
460
|
markRoomMutationComplete();
|
|
537
461
|
}
|
|
538
462
|
catch (error) {
|
|
@@ -632,7 +556,7 @@ export async function joinVault(storage, vaultId, options) {
|
|
|
632
556
|
if (membership === "join")
|
|
633
557
|
return { vaultId, joined: true };
|
|
634
558
|
try {
|
|
635
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(
|
|
559
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => storage.getClient().joinRoom(vaultId), signal), signal);
|
|
636
560
|
}
|
|
637
561
|
catch (err) {
|
|
638
562
|
if (err instanceof MutationOutcomeUnknownError)
|
|
@@ -765,7 +689,7 @@ export async function declineInvite(storage, vaultId, options) {
|
|
|
765
689
|
return { vaultId, declined: false };
|
|
766
690
|
}
|
|
767
691
|
try {
|
|
768
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(
|
|
692
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => client.leave(vaultId), signal), signal);
|
|
769
693
|
}
|
|
770
694
|
catch (error) {
|
|
771
695
|
let suppress = isGoneError(error);
|
|
@@ -777,7 +701,7 @@ export async function declineInvite(storage, vaultId, options) {
|
|
|
777
701
|
throw error;
|
|
778
702
|
}
|
|
779
703
|
try {
|
|
780
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(
|
|
704
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => client.forget(vaultId), signal), signal);
|
|
781
705
|
}
|
|
782
706
|
catch (error) {
|
|
783
707
|
let suppress = isGoneError(error);
|
|
@@ -815,7 +739,6 @@ export async function shareVault(storage, vaultId, userId, role, options) {
|
|
|
815
739
|
ensureOperationActive(operation.signal);
|
|
816
740
|
const tree = await resolveTree(storage, vaultId, operation.signal);
|
|
817
741
|
const pending = withTreeMutation(storage.getClient(), async () => {
|
|
818
|
-
const client = storage.getClient();
|
|
819
742
|
try {
|
|
820
743
|
ensureOperationActive(operation.signal);
|
|
821
744
|
await storage.refreshRoomState(tree.id, { signal: operation.signal });
|
|
@@ -839,7 +762,7 @@ export async function shareVault(storage, vaultId, userId, role, options) {
|
|
|
839
762
|
if (isActiveMembership(currentMembership ?? ""))
|
|
840
763
|
continue;
|
|
841
764
|
try {
|
|
842
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(
|
|
765
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => space.invite(userId), operation.signal), operation.signal);
|
|
843
766
|
completedRoomIds.add(space.id);
|
|
844
767
|
}
|
|
845
768
|
catch (error) {
|
|
@@ -863,7 +786,7 @@ export async function shareVault(storage, vaultId, userId, role, options) {
|
|
|
863
786
|
if (members.some((member) => member.userId === userId && member.role === "owner")) {
|
|
864
787
|
throw new StorageError("share will not demote an existing owner");
|
|
865
788
|
}
|
|
866
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(
|
|
789
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => space.setPermissions(userId, role), operation.signal), operation.signal);
|
|
867
790
|
completedRoomIds.add(space.id);
|
|
868
791
|
}
|
|
869
792
|
}
|
|
@@ -925,7 +848,7 @@ export async function unshareVault(storage, vaultId, userId, options) {
|
|
|
925
848
|
if (!isRevocableMembership(membership))
|
|
926
849
|
continue;
|
|
927
850
|
try {
|
|
928
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(
|
|
851
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => storage.getClient().kick(space.id, userId, "unshared"), operation.signal), operation.signal);
|
|
929
852
|
completedRoomIds.add(space.id);
|
|
930
853
|
}
|
|
931
854
|
catch (error) {
|
|
@@ -1031,8 +954,7 @@ async function renameTree(storage, treeId, name, options) {
|
|
|
1031
954
|
return withOperationDeadline(options, async (signal) => {
|
|
1032
955
|
const tree = await resolveTree(storage, treeId, signal);
|
|
1033
956
|
try {
|
|
1034
|
-
|
|
1035
|
-
await withRateLimitRetry(() => client ? withMatrixMutationAbort(client, () => tree.setName(name), signal) : tree.setName(name), signal);
|
|
957
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => tree.setName(name), signal), signal);
|
|
1036
958
|
await waitForCondition(async () => {
|
|
1037
959
|
ensureOperationActive(signal);
|
|
1038
960
|
const current = storage.getTree(treeId);
|
|
@@ -1070,38 +992,20 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1070
992
|
if (isTreeDeleted(client, treeId))
|
|
1071
993
|
return { id: treeId, deleted: true };
|
|
1072
994
|
const tree = await resolveTree(storage, treeId, operation.signal);
|
|
1073
|
-
|
|
1074
|
-
// client queue; a preceding delete may have populated the set while we
|
|
1075
|
-
// were waiting for its turn.
|
|
1076
|
-
const removedRooms = getDeletedTreeIds(client);
|
|
1077
|
-
let graph;
|
|
995
|
+
let externalParents;
|
|
1078
996
|
try {
|
|
1079
|
-
//
|
|
1080
|
-
// before
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
await refreshDeletionRooms(storage, tree.id, operation.signal);
|
|
1084
|
-
const spaces = snapshotTreeSpaces(tree, client);
|
|
1085
|
-
const activeSpaces = spaces.filter((space) => !removedRooms.has(space.id));
|
|
1086
|
-
const ids = new Set(activeSpaces.map((space) => space.id));
|
|
1087
|
-
if (!ids.has(tree.id)) {
|
|
1088
|
-
if (removedRooms.has(tree.id)) {
|
|
1089
|
-
return { id: treeId, deleted: true };
|
|
1090
|
-
}
|
|
1091
|
-
throw new StorageError("delete graph is unsafe");
|
|
1092
|
-
}
|
|
1093
|
-
assertTreeEmptyForDeletion(storage, tree, activeSpaces);
|
|
1094
|
-
graph = validateDeletionGraph(client, tree, ids, removedRooms);
|
|
997
|
+
// Deletion owns one room. Refresh its current state and its one
|
|
998
|
+
// supported parent before checking files/folders or changing links.
|
|
999
|
+
externalParents = await refreshDeletionRooms(storage, tree.id, operation.signal);
|
|
1000
|
+
assertTreeEmptyForDeletion(storage, tree);
|
|
1095
1001
|
}
|
|
1096
1002
|
catch (error) {
|
|
1097
1003
|
if (error instanceof StorageError)
|
|
1098
1004
|
throw error;
|
|
1099
1005
|
throw new StorageError("delete failed", { cause: error });
|
|
1100
1006
|
}
|
|
1101
|
-
// Keep external parent links intact until the room deletion succeeds.
|
|
1102
|
-
// Unlinking first would silently detach a live vault on failure.
|
|
1103
1007
|
try {
|
|
1104
|
-
await unlinkExternalParents(storage, tree.id,
|
|
1008
|
+
await unlinkExternalParents(storage, tree.id, externalParents, operation.signal);
|
|
1105
1009
|
}
|
|
1106
1010
|
catch (error) {
|
|
1107
1011
|
if (error instanceof StorageError)
|
|
@@ -1112,11 +1016,8 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1112
1016
|
await deleteRoomDeterministically(storage, tree.id, operation.signal);
|
|
1113
1017
|
}
|
|
1114
1018
|
catch (error) {
|
|
1115
|
-
// The root is still live when its relation is removed. If deletion then
|
|
1116
|
-
// fails, restore both sides before reporting failure; otherwise a live
|
|
1117
|
-
// vault would be silently detached from its external parent.
|
|
1118
1019
|
try {
|
|
1119
|
-
await relinkExternalParents(storage, tree.id,
|
|
1020
|
+
await relinkExternalParents(storage, tree.id, externalParents, new AbortController().signal);
|
|
1120
1021
|
}
|
|
1121
1022
|
catch (cleanupError) {
|
|
1122
1023
|
throw new StorageError("delete cleanup is incomplete", {
|
|
@@ -1149,8 +1050,7 @@ export async function renameFile(storage, treeId, fileId, name, options) {
|
|
|
1149
1050
|
const tree = await resolveTree(storage, treeId, signal);
|
|
1150
1051
|
const branch = await resolveFile(storage, tree, fileId, signal);
|
|
1151
1052
|
try {
|
|
1152
|
-
|
|
1153
|
-
await withRateLimitRetry(() => client ? withMatrixMutationAbort(client, () => branch.setName(name), signal) : branch.setName(name), signal);
|
|
1053
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => branch.setName(name), signal), signal);
|
|
1154
1054
|
// `setName` resolves when the homeserver accepts the state event, but a
|
|
1155
1055
|
// fresh CLI/UI process can still read the previous local room state for a
|
|
1156
1056
|
// short time. Do not report success until this client has observed the new
|
|
@@ -1181,7 +1081,7 @@ async function deleteFileMedia(storage, mediaIds, signal) {
|
|
|
1181
1081
|
const client = storage.getClient();
|
|
1182
1082
|
const http = requireFileDeletionTransport(client);
|
|
1183
1083
|
try {
|
|
1184
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(
|
|
1084
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => http.authedRequest(Method.Post, "/io.telecrypt.storage/delete_media", undefined, { media_ids: [...mediaIds] }, { prefix: ClientPrefix.Unstable, rawResponseBody: true, abortSignal: signal }), signal, "delete file media"), signal);
|
|
1185
1085
|
}
|
|
1186
1086
|
catch (error) {
|
|
1187
1087
|
if (error instanceof MutationOutcomeUnknownError)
|
|
@@ -1209,11 +1109,9 @@ export async function deleteFile(storage, treeId, fileId, options) {
|
|
|
1209
1109
|
await deleteFileMedia(storage, [mediaId], signal);
|
|
1210
1110
|
const completedIds = [];
|
|
1211
1111
|
try {
|
|
1212
|
-
const client =
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(client, () => client.sendStateEvent(tree.id, UNSTABLE_MSC3089_BRANCH.name, {}, branch.id), signal, "delete file state"), signal);
|
|
1216
|
-
await withRateLimitRetry(() => withMatrixMutationAbort(client, () => client.redactEvent(tree.id, branch.id), signal, "delete file event"), signal);
|
|
1112
|
+
const client = storage.getClient();
|
|
1113
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => client.sendStateEvent(tree.id, UNSTABLE_MSC3089_BRANCH.name, {}, branch.id), signal, "delete file state"), signal);
|
|
1114
|
+
await withRateLimitRetry(() => withMatrixMutationAbort(() => client.redactEvent(tree.id, branch.id), signal, "delete file event"), signal);
|
|
1217
1115
|
completedIds.push(branch.id);
|
|
1218
1116
|
}
|
|
1219
1117
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telecrypt-io/storage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"license": "BUSL-1.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@11.19.0",
|
|
@@ -25,12 +25,10 @@
|
|
|
25
25
|
"node": "24.20.0"
|
|
26
26
|
},
|
|
27
27
|
"publishConfig": {
|
|
28
|
-
"access": "public"
|
|
29
|
-
"provenance": true
|
|
28
|
+
"access": "public"
|
|
30
29
|
},
|
|
31
30
|
"scripts": {
|
|
32
31
|
"build": "tsc",
|
|
33
|
-
"typecheck:tests": "tsc -p tsconfig.test.json",
|
|
34
32
|
"lint": "eslint src test --ext .ts",
|
|
35
33
|
"test": "vitest run",
|
|
36
34
|
"test:unit": "vitest run --config vitest.unit.config.ts",
|