@telecrypt-io/storage 0.7.0 → 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 +1 -1
- package/dist/core/constants.d.ts +2 -0
- package/dist/core/constants.js +13 -5
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +1 -0
- package/dist/core/oidc.d.ts +2 -0
- package/dist/core/oidc.js +25 -5
- package/dist/core/operations.js +62 -153
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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
|
package/dist/core/constants.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export declare const MAX_MATRIX_IDENTIFIER_BYTES = 255;
|
|
|
9
9
|
* backend hostname that differs from the user-visible Matrix server name.
|
|
10
10
|
*/
|
|
11
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;
|
|
12
14
|
export declare function validateMatrixDeviceId(value: unknown): string;
|
|
13
15
|
export declare function validateMatrixRoomId(value: unknown, name?: string): string;
|
|
14
16
|
export declare function validateMatrixEventId(value: unknown, name?: string): string;
|
package/dist/core/constants.js
CHANGED
|
@@ -15,17 +15,25 @@ const MATRIX_EVENT_ID_PATTERN = /^\$[A-Za-z0-9._~:/+\-]+$/;
|
|
|
15
15
|
* backend hostname that differs from the user-visible Matrix server name.
|
|
16
16
|
*/
|
|
17
17
|
export function validateCanonicalMatrixUserId(value, serverName) {
|
|
18
|
-
|
|
19
|
-
new TextEncoder().encode(value).byteLength > MAX_MATRIX_IDENTIFIER_BYTES) {
|
|
20
|
-
throw new Error("invalid Matrix user ID");
|
|
21
|
-
}
|
|
18
|
+
const userId = validateMatrixUserId(value);
|
|
22
19
|
if (typeof serverName !== "string" || !MATRIX_SERVER_NAME_REGEXP.test(serverName)) {
|
|
23
20
|
throw new Error("invalid Matrix server name");
|
|
24
21
|
}
|
|
25
|
-
const match =
|
|
22
|
+
const match = userId.match(MATRIX_USER_ID_PATTERN);
|
|
26
23
|
if (!match || match[2].toLowerCase() !== serverName.toLowerCase()) {
|
|
27
24
|
throw new Error("invalid Matrix user ID for this homeserver");
|
|
28
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
|
+
}
|
|
29
37
|
return value;
|
|
30
38
|
}
|
|
31
39
|
export function validateMatrixDeviceId(value) {
|
package/dist/core/index.d.ts
CHANGED
|
@@ -4,4 +4,6 @@ export * from "./operations.js";
|
|
|
4
4
|
export * from "./errors.js";
|
|
5
5
|
export * from "./oidc.js";
|
|
6
6
|
export * from "./constants.js";
|
|
7
|
+
export { cancelResponseBody, readResponseBody, ResponseBodyReadError } from "./http.js";
|
|
8
|
+
export type { ResponseBody, ResponseBodyOptions } from "./http.js";
|
|
7
9
|
export { isValidName, validateName } from "./validation.js";
|
package/dist/core/index.js
CHANGED
|
@@ -4,4 +4,5 @@ export * from "./operations.js";
|
|
|
4
4
|
export * from "./errors.js";
|
|
5
5
|
export * from "./oidc.js";
|
|
6
6
|
export * from "./constants.js";
|
|
7
|
+
export { cancelResponseBody, readResponseBody, ResponseBodyReadError } from "./http.js";
|
|
7
8
|
export { isValidName, validateName } from "./validation.js";
|
package/dist/core/oidc.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export type OidcTokenEndpointMetadata = Pick<OidcClientConfig, "issuer" | "token
|
|
|
34
34
|
* identifiers, or control characters from an untrusted response to cross the
|
|
35
35
|
* SDK boundary. */
|
|
36
36
|
export declare function sanitizeDiagnosticText(value: string): string;
|
|
37
|
+
/** Validates an endpoint against the configured homeserver and optional issuer path. */
|
|
38
|
+
export declare function assertOidcEndpoint(value: unknown, trustedHomeserver: string, name: string, issuer?: URL, allowQuery?: boolean): string;
|
|
37
39
|
/**
|
|
38
40
|
* Discovers the OIDC/MAS issuer and endpoints for a homeserver via the
|
|
39
41
|
* Matrix client's stable `/auth_metadata` endpoint. Matrix 42 performs this
|
package/dist/core/oidc.js
CHANGED
|
@@ -382,7 +382,7 @@ function abortableDelay(milliseconds, signal) {
|
|
|
382
382
|
schedule();
|
|
383
383
|
});
|
|
384
384
|
}
|
|
385
|
-
function parseHttpUrl(value, name) {
|
|
385
|
+
function parseHttpUrl(value, name, allowQuery = false) {
|
|
386
386
|
const text = requireOpaqueString(value, name);
|
|
387
387
|
let parsed;
|
|
388
388
|
try {
|
|
@@ -396,7 +396,7 @@ function parseHttpUrl(value, name) {
|
|
|
396
396
|
(parsed.protocol === "http:" && !isLoopbackHostname(parsed.hostname)) ||
|
|
397
397
|
parsed.username !== "" ||
|
|
398
398
|
parsed.password !== "" ||
|
|
399
|
-
parsed.search !== "" ||
|
|
399
|
+
(!allowQuery && parsed.search !== "") ||
|
|
400
400
|
parsed.hash !== "" ||
|
|
401
401
|
(parsed.toString() !== text && !isRootWithoutSlash)) {
|
|
402
402
|
throw new StorageError(`OIDC authorization context has an invalid ${name}`);
|
|
@@ -409,6 +409,15 @@ function isWithinIssuerPath(issuer, endpoint) {
|
|
|
409
409
|
const prefix = issuer.pathname.endsWith("/") ? issuer.pathname : `${issuer.pathname}/`;
|
|
410
410
|
return endpoint.pathname === issuer.pathname || endpoint.pathname.startsWith(prefix);
|
|
411
411
|
}
|
|
412
|
+
/** Validates an endpoint against the configured homeserver and optional issuer path. */
|
|
413
|
+
export function assertOidcEndpoint(value, trustedHomeserver, name, issuer, allowQuery = false) {
|
|
414
|
+
const homeserver = parseHttpUrl(trustedHomeserver, "homeserver URL");
|
|
415
|
+
const endpoint = parseHttpUrl(value, name, allowQuery);
|
|
416
|
+
if (endpoint.origin !== homeserver.origin || (issuer && !isWithinIssuerPath(issuer, endpoint))) {
|
|
417
|
+
throw new StorageError(`${name} must remain on the configured OIDC origin and issuer path`);
|
|
418
|
+
}
|
|
419
|
+
return endpoint.toString();
|
|
420
|
+
}
|
|
412
421
|
function matrixAuthMetadataEndpoint(homeserver) {
|
|
413
422
|
const basePath = homeserver.pathname.replace(/\/+$/u, "");
|
|
414
423
|
return new URL(`${basePath}/_matrix/client/v1/auth_metadata`, homeserver.origin).toString();
|
|
@@ -640,7 +649,12 @@ export async function discoverOidcIssuer(homeserverBaseUrl, signal) {
|
|
|
640
649
|
throw failure;
|
|
641
650
|
}
|
|
642
651
|
ensureOidcNotCancelled(signal, "discovery");
|
|
643
|
-
|
|
652
|
+
try {
|
|
653
|
+
return validateTrustedAuthMetadata(body);
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
throw responseValidationFailure("discovery", response, responseText ?? "", error);
|
|
657
|
+
}
|
|
644
658
|
}
|
|
645
659
|
catch (err) {
|
|
646
660
|
if (err instanceof AggregateError)
|
|
@@ -1100,14 +1114,20 @@ export async function whoAmI(homeserverUrl, accessToken, serverName, signal) {
|
|
|
1100
1114
|
if (!isRecord(body)) {
|
|
1101
1115
|
throw responseValidationFailure("identity confirmation", response, responseText ?? "", new StorageError("OIDC identity confirmation returned an invalid response"));
|
|
1102
1116
|
}
|
|
1103
|
-
|
|
1117
|
+
let userId;
|
|
1118
|
+
try {
|
|
1119
|
+
userId = validateMatrixUserId(body.user_id, serverName);
|
|
1120
|
+
}
|
|
1121
|
+
catch (error) {
|
|
1122
|
+
throw responseValidationFailure("identity confirmation", response, responseText ?? "", error);
|
|
1123
|
+
}
|
|
1104
1124
|
let deviceId = null;
|
|
1105
1125
|
if (body.device_id !== undefined && body.device_id !== null) {
|
|
1106
1126
|
try {
|
|
1107
1127
|
deviceId = validateMatrixDeviceId(body.device_id);
|
|
1108
1128
|
}
|
|
1109
1129
|
catch (error) {
|
|
1110
|
-
throw new StorageError("OIDC identity confirmation returned an invalid device ID", { cause: error });
|
|
1130
|
+
throw responseValidationFailure("identity confirmation", response, responseText ?? "", new StorageError("OIDC identity confirmation returned an invalid device ID", { cause: error }));
|
|
1111
1131
|
}
|
|
1112
1132
|
}
|
|
1113
1133
|
if (signal?.aborted)
|
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
|
|
@@ -204,9 +204,7 @@ async function resolveFile(storage, tree, fileId, signal) {
|
|
|
204
204
|
* room. This also prevents a folder/vault delete from silently deleting a
|
|
205
205
|
* nested shared tree.
|
|
206
206
|
*/
|
|
207
|
-
function assertTreeEmptyForDeletion(storage, tree
|
|
208
|
-
if (spaces.length !== 1)
|
|
209
|
-
throw new NonEmptyTreeError(tree.id);
|
|
207
|
+
function assertTreeEmptyForDeletion(storage, tree) {
|
|
210
208
|
let files;
|
|
211
209
|
try {
|
|
212
210
|
files = tree.listFiles().filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id));
|
|
@@ -216,40 +214,74 @@ function assertTreeEmptyForDeletion(storage, tree, spaces) {
|
|
|
216
214
|
}
|
|
217
215
|
if (files.length > 0)
|
|
218
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
|
+
}
|
|
219
229
|
}
|
|
220
230
|
async function refreshDeletionRooms(storage, rootId, signal) {
|
|
221
231
|
const client = storage.getClient();
|
|
222
|
-
const http = client.http;
|
|
223
|
-
// A real MatrixClient always exposes its authenticated HTTP transport. A
|
|
224
|
-
// client without one cannot prove that its local relation graph is current.
|
|
225
|
-
if (typeof http?.authedRequest !== "function") {
|
|
226
|
-
throw new StorageError("delete graph refresh is unavailable");
|
|
227
|
-
}
|
|
228
232
|
await storage.refreshRoomState(rootId, { signal });
|
|
229
233
|
if (signal?.aborted)
|
|
230
234
|
throw new StorageError("operation cancelled");
|
|
231
|
-
// The refreshed root owns its parent relation. Verify its one supported
|
|
232
|
-
// external parent directly; unrelated local rooms are outside this deletion.
|
|
233
235
|
const parentEvents = readRelationEvents(client, rootId, EventType.SpaceParent);
|
|
234
236
|
if (parentEvents === null)
|
|
235
|
-
throw new StorageError("
|
|
237
|
+
throw new StorageError("could not enumerate storage parent safely");
|
|
236
238
|
const parentIds = new Set();
|
|
237
239
|
for (const event of parentEvents) {
|
|
238
240
|
if (!isActiveRelationEvent(event))
|
|
239
241
|
continue;
|
|
240
242
|
const parentId = relationStateKey(event);
|
|
241
243
|
if (!parentId)
|
|
242
|
-
throw new StorageError("
|
|
244
|
+
throw new StorageError("could not enumerate storage parent safely");
|
|
243
245
|
if (parentId !== rootId)
|
|
244
246
|
parentIds.add(parentId);
|
|
245
247
|
}
|
|
246
248
|
if (parentIds.size > 1)
|
|
247
|
-
throw new StorageError("
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
250
|
-
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
|
+
}
|
|
251
259
|
if (signal?.aborted)
|
|
252
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;
|
|
253
285
|
}
|
|
254
286
|
async function refreshTreeSpaces(storage, spaces, signal) {
|
|
255
287
|
for (const space of spaces) {
|
|
@@ -280,126 +312,24 @@ function isActiveRelationEvent(event) {
|
|
|
280
312
|
function activeTreeDirectories(client, tree) {
|
|
281
313
|
const childEvents = readRelationEvents(client, tree.id, EventType.SpaceChild);
|
|
282
314
|
if (childEvents === null)
|
|
283
|
-
throw new StorageError("
|
|
315
|
+
throw new StorageError("storage folder state is unavailable");
|
|
284
316
|
const activeChildIds = new Set();
|
|
285
317
|
for (const event of childEvents) {
|
|
286
318
|
if (!isActiveRelationEvent(event))
|
|
287
319
|
continue;
|
|
288
320
|
const childId = relationStateKey(event);
|
|
289
321
|
if (!childId)
|
|
290
|
-
throw new StorageError("
|
|
322
|
+
throw new StorageError("storage folder state is inconsistent");
|
|
291
323
|
activeChildIds.add(childId);
|
|
292
324
|
}
|
|
293
325
|
const knownChildren = new Map(tree.getDirectories().map((child) => [child.id, child]));
|
|
294
326
|
return [...activeChildIds].map((childId) => {
|
|
295
327
|
const child = knownChildren.get(childId);
|
|
296
328
|
if (!child)
|
|
297
|
-
throw new StorageError("
|
|
329
|
+
throw new StorageError("storage folder state is inconsistent");
|
|
298
330
|
return child;
|
|
299
331
|
});
|
|
300
332
|
}
|
|
301
|
-
function snapshotTreeSpaces(root, activeRelationClient) {
|
|
302
|
-
const spaces = [];
|
|
303
|
-
const seen = new Set();
|
|
304
|
-
const pending = [root];
|
|
305
|
-
while (pending.length > 0) {
|
|
306
|
-
const tree = pending.pop();
|
|
307
|
-
if (!tree)
|
|
308
|
-
continue;
|
|
309
|
-
if (seen.has(tree.id))
|
|
310
|
-
continue;
|
|
311
|
-
seen.add(tree.id);
|
|
312
|
-
spaces.push(tree);
|
|
313
|
-
let children;
|
|
314
|
-
try {
|
|
315
|
-
children = activeRelationClient
|
|
316
|
-
? activeTreeDirectories(activeRelationClient, tree)
|
|
317
|
-
: tree.getDirectories();
|
|
318
|
-
}
|
|
319
|
-
catch (error) {
|
|
320
|
-
if (error instanceof StorageError)
|
|
321
|
-
throw error;
|
|
322
|
-
throw new StorageError("could not enumerate storage space descendants safely", { cause: error });
|
|
323
|
-
}
|
|
324
|
-
for (const child of children)
|
|
325
|
-
pending.push(child);
|
|
326
|
-
}
|
|
327
|
-
return spaces;
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Deletion is intentionally narrower than MSC3089TreeSpace.delete(): a local
|
|
331
|
-
* child graph is not proof that the rooms are exclusively owned by this tree.
|
|
332
|
-
* Validate every observed edge in both directions and reject any parent or
|
|
333
|
-
* child edge outside the exact deletion set before the first mutation.
|
|
334
|
-
*/
|
|
335
|
-
function validateDeletionGraph(client, root, ids, deletedRooms) {
|
|
336
|
-
const edges = new Set();
|
|
337
|
-
const externalParents = [];
|
|
338
|
-
const addEdge = (parentId, childId) => {
|
|
339
|
-
if (deletedRooms.has(childId))
|
|
340
|
-
return;
|
|
341
|
-
if (!ids.has(childId))
|
|
342
|
-
throw new StorageError("delete graph is unsafe");
|
|
343
|
-
edges.add(`${parentId}\u0000${childId}`);
|
|
344
|
-
};
|
|
345
|
-
for (const id of ids) {
|
|
346
|
-
const tree = client.unstableGetFileTreeSpace(id);
|
|
347
|
-
if (!tree)
|
|
348
|
-
throw new StorageError("delete graph is unsafe");
|
|
349
|
-
const directories = activeTreeDirectories(client, tree);
|
|
350
|
-
for (const child of directories)
|
|
351
|
-
addEdge(id, child.id);
|
|
352
|
-
const childEvents = readRelationEvents(client, id, EventType.SpaceChild);
|
|
353
|
-
if (childEvents === null)
|
|
354
|
-
throw new StorageError("delete graph is unsafe");
|
|
355
|
-
for (const event of childEvents) {
|
|
356
|
-
if (!isActiveRelationEvent(event))
|
|
357
|
-
continue;
|
|
358
|
-
const childId = relationStateKey(event);
|
|
359
|
-
if (!childId)
|
|
360
|
-
throw new StorageError("delete graph is unsafe");
|
|
361
|
-
addEdge(id, childId);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
for (const edge of edges) {
|
|
365
|
-
const separator = edge.indexOf("\u0000");
|
|
366
|
-
const parentId = edge.slice(0, separator);
|
|
367
|
-
const childId = edge.slice(separator + 1);
|
|
368
|
-
const childLink = readRelationEvents(client, parentId, EventType.SpaceChild, childId);
|
|
369
|
-
const parentLink = readRelationEvents(client, childId, EventType.SpaceParent, parentId);
|
|
370
|
-
if (!childLink?.some((event) => isActiveRelationEvent(event) && relationStateKey(event, childId) === childId) ||
|
|
371
|
-
!parentLink?.some((event) => isActiveRelationEvent(event) && relationStateKey(event, parentId) === parentId)) {
|
|
372
|
-
throw new StorageError("delete graph is unsafe");
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
for (const id of ids) {
|
|
376
|
-
const parentEvents = readRelationEvents(client, id, EventType.SpaceParent);
|
|
377
|
-
if (parentEvents === null)
|
|
378
|
-
throw new StorageError("delete graph is unsafe");
|
|
379
|
-
for (const event of parentEvents) {
|
|
380
|
-
if (!isActiveRelationEvent(event))
|
|
381
|
-
continue;
|
|
382
|
-
const parentId = relationStateKey(event);
|
|
383
|
-
if (!parentId)
|
|
384
|
-
throw new StorageError("delete graph is unsafe");
|
|
385
|
-
if (!ids.has(parentId)) {
|
|
386
|
-
if (id !== root.id || externalParents.includes(parentId)) {
|
|
387
|
-
throw new StorageError("delete graph is unsafe");
|
|
388
|
-
}
|
|
389
|
-
const childLink = readRelationEvents(client, parentId, EventType.SpaceChild, id);
|
|
390
|
-
if (!childLink?.some((candidate) => isActiveRelationEvent(candidate) && relationStateKey(candidate, id) === id)) {
|
|
391
|
-
throw new StorageError("delete graph is unsafe");
|
|
392
|
-
}
|
|
393
|
-
externalParents.push(parentId);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
if (!ids.has(root.id))
|
|
398
|
-
throw new StorageError("delete graph is unsafe");
|
|
399
|
-
if (externalParents.length > 1)
|
|
400
|
-
throw new StorageError("delete graph is unsafe");
|
|
401
|
-
return { externalParents };
|
|
402
|
-
}
|
|
403
333
|
async function unlinkExternalParents(storage, rootId, externalParents, signal) {
|
|
404
334
|
const client = storage.getClient();
|
|
405
335
|
let mutationAttempted = false;
|
|
@@ -481,10 +411,10 @@ async function deleteRoomDeterministically(storage, roomId, signal) {
|
|
|
481
411
|
const client = storage.getClient();
|
|
482
412
|
const tree = storage.getTree(roomId);
|
|
483
413
|
if (!tree)
|
|
484
|
-
throw new StorageError("
|
|
414
|
+
throw new StorageError("storage room is unavailable for deletion");
|
|
485
415
|
const self = client.getUserId();
|
|
486
416
|
if (!self)
|
|
487
|
-
throw new StorageError("
|
|
417
|
+
throw new StorageError("Matrix user identity is unavailable");
|
|
488
418
|
const members = await storage.listMembers(tree, { signal });
|
|
489
419
|
if (members.some((member) => member.userId !== self && member.role === "owner" &&
|
|
490
420
|
(member.membership === "join" || member.membership === "invite" || member.membership === "knock"))) {
|
|
@@ -1062,38 +992,20 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1062
992
|
if (isTreeDeleted(client, treeId))
|
|
1063
993
|
return { id: treeId, deleted: true };
|
|
1064
994
|
const tree = await resolveTree(storage, treeId, operation.signal);
|
|
1065
|
-
|
|
1066
|
-
// client queue; a preceding delete may have populated the set while we
|
|
1067
|
-
// were waiting for its turn.
|
|
1068
|
-
const removedRooms = getDeletedTreeIds(client);
|
|
1069
|
-
let graph;
|
|
995
|
+
let externalParents;
|
|
1070
996
|
try {
|
|
1071
|
-
//
|
|
1072
|
-
// before
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
await refreshDeletionRooms(storage, tree.id, operation.signal);
|
|
1076
|
-
const spaces = snapshotTreeSpaces(tree, client);
|
|
1077
|
-
const activeSpaces = spaces.filter((space) => !removedRooms.has(space.id));
|
|
1078
|
-
const ids = new Set(activeSpaces.map((space) => space.id));
|
|
1079
|
-
if (!ids.has(tree.id)) {
|
|
1080
|
-
if (removedRooms.has(tree.id)) {
|
|
1081
|
-
return { id: treeId, deleted: true };
|
|
1082
|
-
}
|
|
1083
|
-
throw new StorageError("delete graph is unsafe");
|
|
1084
|
-
}
|
|
1085
|
-
assertTreeEmptyForDeletion(storage, tree, activeSpaces);
|
|
1086
|
-
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);
|
|
1087
1001
|
}
|
|
1088
1002
|
catch (error) {
|
|
1089
1003
|
if (error instanceof StorageError)
|
|
1090
1004
|
throw error;
|
|
1091
1005
|
throw new StorageError("delete failed", { cause: error });
|
|
1092
1006
|
}
|
|
1093
|
-
// Keep external parent links intact until the room deletion succeeds.
|
|
1094
|
-
// Unlinking first would silently detach a live vault on failure.
|
|
1095
1007
|
try {
|
|
1096
|
-
await unlinkExternalParents(storage, tree.id,
|
|
1008
|
+
await unlinkExternalParents(storage, tree.id, externalParents, operation.signal);
|
|
1097
1009
|
}
|
|
1098
1010
|
catch (error) {
|
|
1099
1011
|
if (error instanceof StorageError)
|
|
@@ -1104,11 +1016,8 @@ async function deleteTree(storage, treeId, options) {
|
|
|
1104
1016
|
await deleteRoomDeterministically(storage, tree.id, operation.signal);
|
|
1105
1017
|
}
|
|
1106
1018
|
catch (error) {
|
|
1107
|
-
// The root is still live when its relation is removed. If deletion then
|
|
1108
|
-
// fails, restore both sides before reporting failure; otherwise a live
|
|
1109
|
-
// vault would be silently detached from its external parent.
|
|
1110
1019
|
try {
|
|
1111
|
-
await relinkExternalParents(storage, tree.id,
|
|
1020
|
+
await relinkExternalParents(storage, tree.id, externalParents, new AbortController().signal);
|
|
1112
1021
|
}
|
|
1113
1022
|
catch (cleanupError) {
|
|
1114
1023
|
throw new StorageError("delete cleanup is incomplete", {
|