@telecrypt-io/storage 0.5.31 → 0.6.0

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
@@ -19,7 +19,7 @@ is maintained in its own repository.
19
19
  ## Install
20
20
 
21
21
  ```bash
22
- npm install @telecrypt-io/storage@0.5.28
22
+ npm install @telecrypt-io/storage@0.6.0
23
23
  ```
24
24
 
25
25
  This gives you the `TeleCryptIOStorage` library and its browser-safe `core` API.
@@ -91,20 +91,19 @@ a file tree using Matrix primitives:
91
91
  | Vault | A Space (room marked as a file tree) |
92
92
  | Folder | A child Space |
93
93
  | File | An event pointing at encrypted uploaded content |
94
- | Version | A newer event superseding the old |
95
94
  | Sharing | Room invitation |
96
95
  | Permissions | Power levels |
97
96
 
98
97
  ### Deletion
99
98
 
100
99
  Vaults and folders are deleted only when they are empty. Delete each file first; deleting a file
101
- also removes its complete Matrix version chain and reports a typed partial result if cleanup stops
102
- after some versions. Delete empty child folders before deleting their parent. A nonempty delete
100
+ also removes its encrypted media object before redacting its Matrix event. Delete empty child
101
+ folders before deleting their parent. A nonempty delete
103
102
  fails with `NonEmptyTreeError`, so a shared or nested tree is never removed implicitly.
104
103
 
105
104
  Encryption uses the same scheme as Matrix attachments (AES-CTR with a per-file key, keys
106
105
  distributed via the room's Megolm session). File deletion uses TeleCrypt's authenticated Synapse
107
- storage extension to remove the local media and thumbnail objects before Matrix redaction; stock
106
+ storage extension to remove the local media object before Matrix redaction; stock
108
107
  Synapse without that extension cannot provide the SDK deletion contract.
109
108
 
110
109
  ## Development
@@ -20,15 +20,12 @@ export interface TreeSpace {
20
20
  setPermissions(userId: string, role: string): Promise<void>;
21
21
  getFile(fileEventId: string): FileBranch | null;
22
22
  listFiles(): FileBranch[];
23
- listAllFiles(): FileBranch[];
24
23
  createFile(name: string, encryptedContents: ArrayBuffer | Uint8Array, info: Record<string, unknown>, additionalContent?: Record<string, unknown>): Promise<{
25
24
  event_id: string;
26
25
  }>;
27
26
  }
28
27
  export interface FileBranch {
29
28
  readonly id: string;
30
- readonly version: number;
31
- readonly isActive: boolean;
32
29
  getName(): string;
33
30
  setName(name: string): Promise<void>;
34
31
  delete(): Promise<void>;
@@ -37,10 +34,6 @@ export interface FileBranch {
37
34
  httpUrl: string;
38
35
  }>;
39
36
  getFileEvent(): Promise<Pick<MatrixEvent, "getContent" | "getTs" | "isDecryptionFailure">>;
40
- getVersionHistory(): Promise<FileBranch[]>;
41
- createNewVersion(name: string, encryptedContents: ArrayBuffer | Uint8Array, info: Record<string, unknown>, additionalContent?: Record<string, unknown>): Promise<{
42
- event_id: string;
43
- }>;
44
37
  }
45
38
  export interface CreateTeleCryptIOStorageOptions {
46
39
  /** Matrix homeserver base URL, e.g. "https://matrix.example.com". */
@@ -16,7 +16,6 @@ import { ClientPrefix } from "matrix-js-sdk/lib/http-api/prefix.js";
16
16
  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
- import { validateMatrixEventId } from "./constants.js";
20
19
  import { validateName } from "./validation.js";
21
20
  import { getDeletedTreeIds, isFileDeleted, isTreeDeleted, markFileDeleted, markTreeDeleted, } from "../deletion-markers.js";
22
21
  /**
@@ -206,20 +205,17 @@ async function resolveFile(storage, tree, fileId, signal) {
206
205
  }
207
206
  }
208
207
  /**
209
- * Deleting a tree is deliberately a one-room operation. File versions are
210
- * physical Matrix media events, so callers must delete files explicitly and
211
- * observe each result before removing their containing room. This also
212
- * prevents a folder/vault delete from silently deleting a nested shared tree.
208
+ * Deleting a tree is deliberately a one-room operation. Callers must delete
209
+ * files explicitly and observe each result before removing their containing
210
+ * room. This also prevents a folder/vault delete from silently deleting a
211
+ * nested shared tree.
213
212
  */
214
213
  function assertTreeEmptyForDeletion(storage, tree, spaces) {
215
214
  if (spaces.length !== 1)
216
215
  throw new NonEmptyTreeError(tree.id);
217
216
  let files;
218
217
  try {
219
- files = tree
220
- .listAllFiles()
221
- .filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id) &&
222
- !isConfirmedDeletedFile(storage.getClient(), tree.id, file));
218
+ files = tree.listFiles().filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id));
223
219
  }
224
220
  catch (error) {
225
221
  throw new StorageError("could not enumerate storage files safely", { cause: error });
@@ -336,26 +332,6 @@ function snapshotTreeSpaces(root, activeRelationClient) {
336
332
  }
337
333
  return spaces;
338
334
  }
339
- /**
340
- * A deleted MSC3089 branch is represented by an authoritative empty state
341
- * event. Inactive branches with metadata are historical versions, and a
342
- * missing or malformed state cannot prove that the file was deleted.
343
- */
344
- function isConfirmedDeletedFile(client, treeId, file) {
345
- if (file.isActive)
346
- return false;
347
- const stateEvents = readRelationEvents(client, treeId, UNSTABLE_MSC3089_BRANCH.name, file.id);
348
- if (!stateEvents || stateEvents.length !== 1)
349
- return false;
350
- const stateEvent = stateEvents[0];
351
- if (relationStateKey(stateEvent) !== file.id)
352
- return false;
353
- const content = stateEvent.getContent?.();
354
- return (typeof content === "object" &&
355
- content !== null &&
356
- !Array.isArray(content) &&
357
- Object.keys(content).length === 0);
358
- }
359
335
  /**
360
336
  * Deletion is intentionally narrower than MSC3089TreeSpace.delete(): a local
361
337
  * child graph is not proof that the rooms are exclusively owned by this tree.
@@ -1194,61 +1170,6 @@ export async function renameFile(storage, treeId, fileId, name, options) {
1194
1170
  return { id: fileId, name };
1195
1171
  }, "mutation");
1196
1172
  }
1197
- /**
1198
- * Resolve the complete encrypted version chain before mutating Matrix.
1199
- *
1200
- * matrix-js-sdk returns the active version first and older versions after it.
1201
- * Treat a repeated event ID as a cycle/invalid relation graph, and bound both
1202
- * the event walk and the media identifiers sent to Synapse. Resolving every
1203
- * media URL first is important: a decryption or relation failure must not leave
1204
- * a partially deleted file chain.
1205
- */
1206
- async function resolveFileVersions(branch, signal) {
1207
- let history;
1208
- try {
1209
- history = await branch.getVersionHistory();
1210
- }
1211
- catch (error) {
1212
- throw new StorageError("could not resolve file version history safely", { cause: error });
1213
- }
1214
- if (!Array.isArray(history) || history.length === 0) {
1215
- throw new StorageError("file version history is invalid");
1216
- }
1217
- const eventIds = new Set();
1218
- const mediaIds = new Set();
1219
- const versions = [];
1220
- for (const version of history) {
1221
- if (signal?.aborted)
1222
- throw new StorageError("operation cancelled");
1223
- if (!version || typeof version.id !== "string") {
1224
- throw new StorageError("file version history is invalid");
1225
- }
1226
- try {
1227
- validateMatrixEventId(version.id, "file version event ID");
1228
- }
1229
- catch (error) {
1230
- throw new StorageError("file version history contains an invalid event ID", { cause: error });
1231
- }
1232
- if (eventIds.has(version.id)) {
1233
- throw new StorageError("file version history contains a cycle");
1234
- }
1235
- eventIds.add(version.id);
1236
- let fileInfo;
1237
- try {
1238
- fileInfo = await version.getFileInfo();
1239
- }
1240
- catch (error) {
1241
- throw new StorageError("could not resolve encrypted file version safely", { cause: error });
1242
- }
1243
- const mediaId = fileInfo?.info?.url;
1244
- if (typeof mediaId !== "string" || mediaId.length === 0) {
1245
- throw new StorageError("encrypted file version has no media identifier");
1246
- }
1247
- mediaIds.add(mediaId);
1248
- versions.push({ branch: version, mediaId });
1249
- }
1250
- return versions;
1251
- }
1252
1173
  function requireFileDeletionTransport(client) {
1253
1174
  const http = client.http;
1254
1175
  if (!http || typeof http.authedRequest !== "function") {
@@ -1274,19 +1195,26 @@ export async function deleteFile(storage, treeId, fileId, options) {
1274
1195
  return withOperationDeadline(options, async (signal) => {
1275
1196
  const tree = await resolveTree(storage, treeId, signal);
1276
1197
  const branch = await resolveFile(storage, tree, fileId, signal);
1277
- const versions = await resolveFileVersions(branch, signal);
1278
- const mediaIds = [...new Set(versions.map(({ mediaId }) => mediaId))];
1279
- await deleteFileMedia(storage, mediaIds, signal);
1198
+ let fileInfo;
1199
+ try {
1200
+ fileInfo = await branch.getFileInfo();
1201
+ }
1202
+ catch (error) {
1203
+ throw new StorageError("could not resolve encrypted file safely", { cause: error });
1204
+ }
1205
+ const mediaId = fileInfo?.info?.url;
1206
+ if (typeof mediaId !== "string" || mediaId.length === 0) {
1207
+ throw new StorageError("encrypted file has no media identifier");
1208
+ }
1209
+ await deleteFileMedia(storage, [mediaId], signal);
1280
1210
  const completedIds = [];
1281
1211
  try {
1282
1212
  const client = typeof storage.getClient === "function" ? storage.getClient() : undefined;
1283
1213
  if (!client)
1284
1214
  throw new StorageError("Matrix client unavailable");
1285
- for (const { branch: version } of versions) {
1286
- await withRateLimitRetry(() => withMatrixMutationAbort(client, () => client.sendStateEvent(tree.id, UNSTABLE_MSC3089_BRANCH.name, {}, version.id), signal, "delete file state"), signal);
1287
- await withRateLimitRetry(() => withMatrixMutationAbort(client, () => client.redactEvent(tree.id, version.id), signal, "delete file event"), signal);
1288
- completedIds.push(version.id);
1289
- }
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);
1217
+ completedIds.push(branch.id);
1290
1218
  }
1291
1219
  catch (error) {
1292
1220
  if (error instanceof MutationOutcomeUnknownError)
@@ -1301,18 +1229,16 @@ export async function deleteFile(storage, treeId, fileId, options) {
1301
1229
  await storage.refreshRoomState(tree.id, { signal });
1302
1230
  ensureOperationActive(signal);
1303
1231
  const current = tree.getFile(fileId);
1304
- return !current || !current.isActive ? true : null;
1232
+ return !current ? true : null;
1305
1233
  }, { timeoutMs: 15000, signal });
1306
1234
  }
1307
1235
  catch (error) {
1308
1236
  if (error instanceof MutationOutcomeUnknownError)
1309
1237
  throw error;
1310
- const detail = "Matrix deletion completed but the inactive file state could not be verified";
1238
+ const detail = "Matrix deletion completed but the file state could not be verified";
1311
1239
  throw new MutationPartialError("delete file", completedIds, detail, { cause: error });
1312
1240
  }
1313
- for (const { branch: version } of versions) {
1314
- markFileDeleted(storage.getClient(), tree.id, version.id);
1315
- }
1241
+ markFileDeleted(storage.getClient(), tree.id, branch.id);
1316
1242
  ensureOperationActive(signal);
1317
1243
  return { id: fileId, deleted: true };
1318
1244
  }, "mutation");
@@ -1340,7 +1266,7 @@ export async function uploadFile(storage, treeId, name, bytes, mimetype, options
1340
1266
  await storage.refreshRoomState(tree.id, { signal });
1341
1267
  ensureOperationActive(signal);
1342
1268
  const file = tree.getFile(fileId);
1343
- return file?.isActive ? file : null;
1269
+ return file ?? null;
1344
1270
  }, { timeoutMs: 15000, signal });
1345
1271
  }
1346
1272
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telecrypt-io/storage",
3
- "version": "0.5.31",
3
+ "version": "0.6.0",
4
4
  "license": "BUSL-1.1",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.19.0",