@telecrypt-io/storage 0.5.27 → 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 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.10
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)
@@ -4,3 +4,4 @@ export * from "./operations.js";
4
4
  export * from "./errors.js";
5
5
  export * from "./oidc.js";
6
6
  export * from "./constants.js";
7
+ export { isValidName, MAX_NAME_LENGTH, validateName } from "./validation.js";
@@ -4,3 +4,4 @@ export * from "./operations.js";
4
4
  export * from "./errors.js";
5
5
  export * from "./oidc.js";
6
6
  export * from "./constants.js";
7
+ export { isValidName, MAX_NAME_LENGTH, validateName } from "./validation.js";
@@ -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
- const roomIds = await storage.listJoinedRoomIds({ signal });
269
- if (roomIds.length > MAX_DELETION_ROOMS)
270
- throw new StorageError("delete graph is too large");
271
- const joined = new Set(roomIds);
272
- const roomIdsToRefresh = [];
273
- // Matrix has no bounded endpoint that enumerates every invited/non-joined
274
- // room carrying a space relation. Refresh every locally visible room and
275
- // fail closed for an invite/knock/unknown membership; a joined external
276
- // parent remains inspectable and is validated by validateDeletionGraph.
277
- for (const room of client.getRooms()) {
278
- if (!joined.has(room.roomId)) {
279
- const membership = room.getMyMembership?.();
280
- if (membership !== "join")
281
- throw new StorageError("delete graph inventory is incomplete");
282
- roomIdsToRefresh.push(room.roomId);
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
- roomIdsToRefresh.push(...roomIds);
286
- let nextRoom = 0;
287
- let failed = false;
288
- let firstError;
289
- const refreshWorker = async () => {
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
- externalParents.push(parentId);
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
- throw new StorageError("delete graph is unsafe");
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 client = storage.getClient();
528
- const tree = storage.getTree(roomId);
529
- if (!tree)
530
- throw new StorageError("delete graph is unsafe");
531
- const self = client.getUserId();
532
- if (!self)
533
- throw new StorageError("delete graph is unsafe");
534
- const members = await storage.listMembers(tree, { signal });
535
- if (members.some((member) => member.userId !== self && member.role === "owner" &&
536
- (member.membership === "join" || member.membership === "invite" || member.membership === "knock"))) {
537
- throw new StorageError("delete will not kick another room owner");
538
- }
539
- for (const member of members) {
540
- const membership = member.membership;
541
- if (member.userId === self ||
542
- (membership !== "join" && membership !== "invite" && membership !== "knock")) {
543
- continue;
544
- }
545
- try {
546
- await withRateLimitRetry(() => withMatrixMutationAbort(client, () => client.kick(roomId, member.userId, "Room deleted"), signal), signal);
547
- }
548
- catch (error) {
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
- if (error instanceof MatrixError && error.errcode === "M_FORBIDDEN") {
552
- const currentMembership = await storage.getRoomMembership(roomId, member.userId, { signal });
553
- if (currentMembership === "leave" ||
554
- currentMembership === "ban" ||
555
- !currentMembership) {
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.leave(roomId), signal), signal);
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
- try {
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 (!isGoneError(error))
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 every locally visible room before taking the immutable graph
1138
- // snapshot. Local MSC3089 relations and memberships can lag another
1139
- // session; deleting from that stale view can orphan shared descendants.
1140
- await refreshDeletionRooms(storage, operation.signal);
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 && error.message === "operation cancelled")
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 && error.message === "operation cancelled")
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 && error.message === "operation cancelled")
1177
+ if (error instanceof StorageError)
1197
1178
  throw error;
1198
1179
  throw new StorageError("delete failed");
1199
1180
  }
@@ -1,3 +1,4 @@
1
- /** Small shared input validators kept out of the public core barrel. */
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;
@@ -1,10 +1,13 @@
1
- /** Small shared input validators kept out of the public core barrel. */
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 (typeof value !== "string" ||
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telecrypt-io/storage",
3
- "version": "0.5.27",
3
+ "version": "0.5.28",
4
4
  "license": "BUSL-1.1",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.19.0",