@telecrypt-io/storage 0.9.2 → 0.9.3

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
@@ -12,6 +12,9 @@ unmerged Matrix proposal.
12
12
  npm install @telecrypt-io/storage
13
13
  ```
14
14
 
15
+ The SDK supports Node.js `24.21.0` as its exact engine version. Its release tooling uses the
16
+ bundled npm `11.19.0`.
17
+
15
18
  ## Browser quick start
16
19
 
17
20
  The example below starts with an authenticated OIDC session. Obtain `userId`, `accessToken`, and
@@ -170,6 +170,7 @@ export declare class TeleCryptIOStorage {
170
170
  private getStateEvent;
171
171
  private getTreeOwnerId;
172
172
  private sendEncryptedMetadataMessage;
173
+ private requireEncryptedStorageRoom;
173
174
  private readEncryptedMetadataMessage;
174
175
  private writeTreeName;
175
176
  private getTreeMetadataEvent;
@@ -7,7 +7,7 @@ import { FileTooLargeError, RecoveryAlreadyConfiguredError, RecoveryRestoreError
7
7
  import { MAX_MEDIA_FILE_BYTES, MAX_MATRIX_IDENTIFIER_BYTES, validateCanonicalMatrixUserId, validateMatrixDeviceId, validateMatrixEventId, validateMatrixRoomId, } from "./core/constants.js";
8
8
  import { cancelResponseBody, raceWithAbort, readMediaResponseBody, readResponseBody, ResponseBodyReadError, } from "./core/http.js";
9
9
  import { sanitizeDiagnosticText } from "./core/oidc.js";
10
- import { validateName } from "./core/validation.js";
10
+ import { isStorageTreeRoom, isStorageTreeState, STORAGE_ROOM_ENCRYPTION_ALGORITHM, validateName, } from "./core/validation.js";
11
11
  import { ConditionTimeoutError, waitForCondition } from "./core/poll.js";
12
12
  import { isTreeDeleted } from "./deletion-markers.js";
13
13
  import { DecryptionKeySafe } from "./key-safe.js";
@@ -484,9 +484,16 @@ export class TeleCryptIOStorage {
484
484
  return ownerId;
485
485
  }
486
486
  async sendEncryptedMetadataMessage(roomId, content) {
487
+ await this.refreshRoomState(roomId);
488
+ this.requireEncryptedStorageRoom(roomId);
487
489
  const response = await this.client.sendMessage(roomId, content);
488
490
  return validateMatrixEventId(response?.event_id, "storage metadata response event ID");
489
491
  }
492
+ requireEncryptedStorageRoom(roomId) {
493
+ if (!isStorageTreeRoom(this.client.getRoom(roomId))) {
494
+ throw new StorageError("storage room encryption is unavailable or unsupported");
495
+ }
496
+ }
490
497
  async readEncryptedMetadataMessage(roomId, eventId, allowRedacted = false) {
491
498
  const rawEvent = await this.client.fetchRoomEvent(roomId, eventId);
492
499
  const event = this.client.getEventMapper()(rawEvent);
@@ -591,7 +598,7 @@ export class TeleCryptIOStorage {
591
598
  }
592
599
  const wire = event.getWireContent();
593
600
  if (event.isRedacted() || event.isDecryptionFailure() ||
594
- wire.algorithm !== "m.megolm.v1.aes-sha2" || typeof wire.session_id !== "string" || !wire.session_id) {
601
+ wire.algorithm !== STORAGE_ROOM_ENCRYPTION_ALGORITHM || typeof wire.session_id !== "string" || !wire.session_id) {
595
602
  throw new StorageError("encrypted storage session is unavailable");
596
603
  }
597
604
  sessionId = wire.session_id;
@@ -745,6 +752,7 @@ export class TeleCryptIOStorage {
745
752
  await this.refreshRoomState(treeId, { signal });
746
753
  if (signal?.aborted)
747
754
  throw new StorageError("operation cancelled");
755
+ this.requireEncryptedStorageRoom(treeId);
748
756
  const bytes = encryptedContents instanceof Uint8Array
749
757
  ? encryptedContents
750
758
  : new Uint8Array(encryptedContents);
@@ -916,8 +924,8 @@ export class TeleCryptIOStorage {
916
924
  const event = (type, key = "") => state.find((item) => item.type === type && item.state_key === key);
917
925
  const create = event(EventType.RoomCreate);
918
926
  const purpose = event(UNSTABLE_MSC3088_PURPOSE.name, UNSTABLE_MSC3089_TREE_SUBTYPE.name);
919
- if (create?.content?.[RoomCreateTypeField] !== RoomType.Space ||
920
- purpose?.content?.[UNSTABLE_MSC3088_ENABLED.name] !== true)
927
+ const encryption = event(EventType.RoomEncryption);
928
+ if (!isStorageTreeState(create?.content, purpose?.content, encryption?.content))
921
929
  return false;
922
930
  const userId = this.client.getUserId();
923
931
  if (!userId || create?.sender !== userId ||
@@ -1559,7 +1567,7 @@ export class TeleCryptIOStorage {
1559
1567
  {
1560
1568
  type: EventType.RoomEncryption,
1561
1569
  state_key: "",
1562
- content: { algorithm: "m.megolm.v1.aes-sha2" },
1570
+ content: { algorithm: STORAGE_ROOM_ENCRYPTION_ALGORITHM },
1563
1571
  },
1564
1572
  ],
1565
1573
  };
@@ -1572,8 +1580,9 @@ export class TeleCryptIOStorage {
1572
1580
  throw new StorageError("operation cancelled");
1573
1581
  try {
1574
1582
  const tree = this.client.unstableGetFileTreeSpace(roomId);
1575
- if (tree)
1583
+ if (tree && isStorageTreeRoom(this.client.getRoom(roomId))) {
1576
1584
  return this.decorateTreeSpace(tree);
1585
+ }
1577
1586
  }
1578
1587
  catch (error) {
1579
1588
  lastError = error;
@@ -1613,13 +1622,29 @@ export class TeleCryptIOStorage {
1613
1622
  return tree;
1614
1623
  this.decoratedTreeSpaces.add(tree);
1615
1624
  const originalGetDirectories = tree.getDirectories.bind(tree);
1625
+ const originalGetDirectory = typeof tree.getDirectory === "function"
1626
+ ? tree.getDirectory.bind(tree)
1627
+ : undefined;
1616
1628
  const originalListFiles = typeof tree.listFiles === "function"
1617
1629
  ? tree.listFiles.bind(tree)
1618
1630
  : () => [];
1619
1631
  const originalGetFile = typeof tree.getFile === "function"
1620
1632
  ? tree.getFile.bind(tree)
1621
1633
  : () => null;
1622
- tree.getDirectories = () => originalGetDirectories().map((child) => this.decorateTreeSpace(child));
1634
+ tree.getDirectories = () => originalGetDirectories()
1635
+ .filter((child) => !isTreeDeleted(this.client, child.id) &&
1636
+ isStorageTreeRoom(this.client.getRoom(child.id)))
1637
+ .map((child) => this.decorateTreeSpace(child));
1638
+ if (originalGetDirectory) {
1639
+ tree.getDirectory = (roomId) => {
1640
+ const child = originalGetDirectory(roomId);
1641
+ return child &&
1642
+ !isTreeDeleted(this.client, child.id) &&
1643
+ isStorageTreeRoom(this.client.getRoom(child.id))
1644
+ ? this.decorateTreeSpace(child)
1645
+ : undefined;
1646
+ };
1647
+ }
1623
1648
  tree.createDirectory = (name) => this.createSubtree(tree, name);
1624
1649
  tree.setName = (name) => this.writeTreeName(tree.id, name);
1625
1650
  tree.listFiles = () => originalListFiles().map((branch) => this.decorateFileBranch(tree.id, branch));
@@ -1782,14 +1807,16 @@ export class TeleCryptIOStorage {
1782
1807
  throw new StorageError("operation cancelled");
1783
1808
  if (isTreeDeleted(this.client, room.roomId))
1784
1809
  continue;
1785
- const tree = this.client.unstableGetFileTreeSpace(room.roomId);
1810
+ let tree = this.getTree(room.roomId);
1786
1811
  if (tree) {
1787
1812
  // Existing owner sessions repair the persisted permissions on load;
1788
1813
  // readers never attempt this owner-only mutation.
1789
1814
  if (this.getStateEvent(room.roomId, EventType.RoomCreate, "")?.getSender() === this.client.getUserId()) {
1790
1815
  await this.refreshRoomState(room.roomId, { signal });
1816
+ tree = this.getTree(room.roomId);
1791
1817
  }
1792
- trees.push(this.decorateTreeSpace(tree));
1818
+ if (tree)
1819
+ trees.push(tree);
1793
1820
  }
1794
1821
  }
1795
1822
  return trees;
@@ -1943,6 +1970,8 @@ export class TeleCryptIOStorage {
1943
1970
  getTree(roomId) {
1944
1971
  if (isTreeDeleted(this.client, roomId))
1945
1972
  return null;
1973
+ if (!isStorageTreeRoom(this.client.getRoom(roomId)))
1974
+ return null;
1946
1975
  const tree = this.client.unstableGetFileTreeSpace(roomId);
1947
1976
  return tree ? this.decorateTreeSpace(tree) : null;
1948
1977
  }
@@ -30,9 +30,9 @@ export interface OidcRegistrationClientMetadata {
30
30
  }
31
31
  /** The issuer-bound endpoints accepted by the token refresh adapter. */
32
32
  export type OidcTokenEndpointMetadata = Pick<OidcClientConfig, "issuer" | "token_endpoint" | "revocation_endpoint">;
33
- /** Keep provider diagnostics useful without allowing credentials, customer
34
- * identifiers, or control characters from an untrusted response to cross the
35
- * SDK boundary. */
33
+ /** Redact recognized secret fields and identifiers in provider diagnostics.
34
+ * Structured JSON uses decoded property names at every depth; the text fallback
35
+ * is limited to patterns recognized in unstructured input. */
36
36
  export declare function sanitizeDiagnosticText(value: string): string;
37
37
  /** Validates an endpoint against the configured homeserver and optional issuer path. */
38
38
  export declare function assertOidcEndpoint(value: unknown, trustedHomeserver: string, name: string, issuer?: URL, allowQuery?: boolean): string;
package/dist/core/oidc.js CHANGED
@@ -64,10 +64,8 @@ const DIAGNOSTIC_SECRET_FIELD = /(?:access[_-]?token|refresh[_-]?token|id[_-]?to
64
64
  const DIAGNOSTIC_EMAIL = /[A-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Z0-9-]+(?:\.[A-Z0-9-]+)+/giu;
65
65
  const DIAGNOSTIC_MXID = /@[A-Z0-9._=+\-/]+:[A-Z0-9.-]+/giu;
66
66
  const DIAGNOSTIC_ULID = /\b[0-9A-HJKMNP-TV-Z]{26}\b/giu;
67
- /** Keep provider diagnostics useful without allowing credentials, customer
68
- * identifiers, or control characters from an untrusted response to cross the
69
- * SDK boundary. */
70
- export function sanitizeDiagnosticText(value) {
67
+ /** Existing fallback for plain or malformed provider diagnostics. */
68
+ function sanitizeUnstructuredDiagnosticText(value) {
71
69
  const quotedValue = `"(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|[^\\s,};\\]]+`;
72
70
  return value
73
71
  .replace(/(Bearer\s+)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s"',}]+)/giu, "$1<redacted>")
@@ -81,6 +79,21 @@ export function sanitizeDiagnosticText(value) {
81
79
  .replace(/[\r\n\u0000-\u001f\u007f-\u009f\u2028\u2029]/gu, " ")
82
80
  .trim();
83
81
  }
82
+ /** Redact recognized secret fields and identifiers in provider diagnostics.
83
+ * Structured JSON uses decoded property names at every depth; the text fallback
84
+ * is limited to patterns recognized in unstructured input. */
85
+ export function sanitizeDiagnosticText(value) {
86
+ try {
87
+ return JSON.stringify(JSON.parse(value), (key, item) => {
88
+ if (DIAGNOSTIC_SECRET_FIELD.test(key))
89
+ return "<redacted>";
90
+ return typeof item === "string" ? sanitizeUnstructuredDiagnosticText(item) : item;
91
+ });
92
+ }
93
+ catch {
94
+ return sanitizeUnstructuredDiagnosticText(value);
95
+ }
96
+ }
84
97
  class OidcResponseError extends Error {
85
98
  status;
86
99
  constructor(operation, status, body, cause) {
@@ -11,12 +11,12 @@
11
11
  * platform-specific and stays with the caller.
12
12
  */
13
13
  import { readFileEventMetadata, withMatrixMutationAbort, withTreeMutation, } from "../TeleCryptIOStorage.js";
14
- import { EventType, MatrixError, RoomCreateTypeField, RoomType, UNSTABLE_MSC3088_ENABLED, UNSTABLE_MSC3088_PURPOSE, UNSTABLE_MSC3089_TREE_SUBTYPE, } from "matrix-js-sdk";
14
+ import { EventType, MatrixError, } from "matrix-js-sdk";
15
15
  import { deleteOwnedMedia } from "../media.js";
16
16
  import { validateMatrixEventId } from "./constants.js";
17
17
  import { FileTooLargeError, MutationPartialError, MutationOutcomeUnknownError, NonEmptyTreeError, RoomCreationAmbiguousError, RoomCleanupIncompleteError, StorageError, } from "./errors.js";
18
18
  import { ConditionTimeoutError, waitForCondition } from "./poll.js";
19
- import { validateName } from "./validation.js";
19
+ import { hasStorageTreeMarkersRoom, isStorageTreeRoom, validateName } from "./validation.js";
20
20
  import { prepareReaderHistorySharing } from "../sharing.js";
21
21
  import { isFileDeleted, isTreeDeleted, markFileDeleted, markTreeDeleted, } from "../deletion-markers.js";
22
22
  /**
@@ -660,14 +660,6 @@ function hasActiveSpaceParent(room) {
660
660
  const events = result == null ? [] : Array.isArray(result) ? result : [result];
661
661
  return events.some((event) => isActiveRelationEvent(event));
662
662
  }
663
- function isReviewedStorageTreeRoom(room) {
664
- const createEvent = room?.currentState?.getStateEvents?.(EventType.RoomCreate, "");
665
- const createContent = createEvent?.getContent?.();
666
- const purposeEvent = room?.currentState?.getStateEvents?.(UNSTABLE_MSC3088_PURPOSE.name, UNSTABLE_MSC3089_TREE_SUBTYPE.name);
667
- const purposeContent = purposeEvent?.getContent?.();
668
- return (createContent?.[RoomCreateTypeField] === RoomType.Space &&
669
- purposeContent?.[UNSTABLE_MSC3088_ENABLED.name] === true);
670
- }
671
663
  /** Rooms where this account is invited and the room looks like a file tree. */
672
664
  export async function listPendingInvites(storage, options) {
673
665
  await storage.keySafe.requireReady({ signal: options?.signal });
@@ -695,7 +687,7 @@ export async function listPendingInvites(storage, options) {
695
687
  }
696
688
  // Invite state may not have MSC3089 tree metadata yet — accept rooms whose
697
689
  // create event marks them as a file tree space.
698
- if (isReviewedStorageTreeRoom(room)) {
690
+ if (isStorageTreeRoom(room)) {
699
691
  invites.push({
700
692
  id: room.roomId,
701
693
  name: roomDisplayName(storage, room.roomId),
@@ -734,7 +726,7 @@ export async function declineInvite(storage, vaultId, options) {
734
726
  // requiring a joined TreeSpace object.
735
727
  if (!currentRoom ||
736
728
  hasActiveSpaceParent(currentRoom) ||
737
- !isReviewedStorageTreeRoom(currentRoom)) {
729
+ !hasStorageTreeMarkersRoom(currentRoom)) {
738
730
  throw new StorageError("decline failed");
739
731
  }
740
732
  let membership;
@@ -1,3 +1,18 @@
1
1
  /** Shared name contract used by SDK inputs and remote projections. */
2
2
  export declare function isValidName(value: unknown): value is string;
3
3
  export declare function validateName(value: unknown, label: string): asserts value is string;
4
+ export declare const STORAGE_ROOM_ENCRYPTION_ALGORITHM = "m.megolm.v1.aes-sha2";
5
+ interface StorageTreeRoomLike {
6
+ currentState?: {
7
+ getStateEvents?: (eventType: string, stateKey?: string) => unknown;
8
+ };
9
+ }
10
+ /** The shared Storage-room contract for both synced rooms and refreshed state. */
11
+ export declare function isStorageTreeState(createContent: unknown, purposeContent: unknown, encryptionContent: unknown): boolean;
12
+ /** The room markers needed for explicit invite cleanup before encryption is available. */
13
+ export declare function hasStorageTreeMarkers(createContent: unknown, purposeContent: unknown): boolean;
14
+ /** Checks the existing Storage markers and Megolm room encryption together. */
15
+ export declare function isStorageTreeRoom(room: StorageTreeRoomLike | null | undefined): boolean;
16
+ /** Checks only the room markers for explicit cleanup paths such as declining an invite. */
17
+ export declare function hasStorageTreeMarkersRoom(room: StorageTreeRoomLike | null | undefined): boolean;
18
+ export {};
@@ -1,3 +1,4 @@
1
+ import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_MSC3088_ENABLED, UNSTABLE_MSC3088_PURPOSE, UNSTABLE_MSC3089_TREE_SUBTYPE, } from "matrix-js-sdk";
1
2
  /** Shared name contract used by SDK inputs and remote projections. */
2
3
  export function isValidName(value) {
3
4
  return (typeof value === "string" &&
@@ -9,3 +10,47 @@ export function validateName(value, label) {
9
10
  throw new Error(`invalid ${label}`);
10
11
  }
11
12
  }
13
+ export const STORAGE_ROOM_ENCRYPTION_ALGORITHM = "m.megolm.v1.aes-sha2";
14
+ function isRecord(value) {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+ /** The shared Storage-room contract for both synced rooms and refreshed state. */
18
+ export function isStorageTreeState(createContent, purposeContent, encryptionContent) {
19
+ return hasStorageTreeMarkers(createContent, purposeContent) &&
20
+ isRecord(encryptionContent) &&
21
+ encryptionContent.algorithm === STORAGE_ROOM_ENCRYPTION_ALGORITHM;
22
+ }
23
+ /** The room markers needed for explicit invite cleanup before encryption is available. */
24
+ export function hasStorageTreeMarkers(createContent, purposeContent) {
25
+ return (isRecord(createContent) &&
26
+ createContent[RoomCreateTypeField] === RoomType.Space &&
27
+ isRecord(purposeContent) &&
28
+ purposeContent[UNSTABLE_MSC3088_ENABLED.name] === true);
29
+ }
30
+ function getStorageTreeState(room) {
31
+ const getContent = (eventType, stateKey) => {
32
+ const raw = room?.currentState?.getStateEvents?.(eventType, stateKey);
33
+ const event = Array.isArray(raw) ? raw[0] : raw;
34
+ if (typeof event !== "object" || event === null)
35
+ return undefined;
36
+ const getEventContent = event.getContent;
37
+ return typeof getEventContent === "function"
38
+ ? getEventContent.call(event)
39
+ : undefined;
40
+ };
41
+ return {
42
+ createContent: getContent(EventType.RoomCreate, ""),
43
+ purposeContent: getContent(UNSTABLE_MSC3088_PURPOSE.name, UNSTABLE_MSC3089_TREE_SUBTYPE.name),
44
+ encryptionContent: getContent(EventType.RoomEncryption, ""),
45
+ };
46
+ }
47
+ /** Checks the existing Storage markers and Megolm room encryption together. */
48
+ export function isStorageTreeRoom(room) {
49
+ const state = getStorageTreeState(room);
50
+ return isStorageTreeState(state.createContent, state.purposeContent, state.encryptionContent);
51
+ }
52
+ /** Checks only the room markers for explicit cleanup paths such as declining an invite. */
53
+ export function hasStorageTreeMarkersRoom(room) {
54
+ const state = getStorageTreeState(room);
55
+ return hasStorageTreeMarkers(state.createContent, state.purposeContent);
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telecrypt-io/storage",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "license": "BUSL-1.1",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.19.0",
@@ -22,7 +22,7 @@
22
22
  "LICENSE"
23
23
  ],
24
24
  "engines": {
25
- "node": "24.20.0"
25
+ "node": "24.21.0"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"
@@ -48,7 +48,7 @@
48
48
  "eslint": "10.9.0",
49
49
  "fake-indexeddb": "6.2.5",
50
50
  "typescript": "5.9.3",
51
- "vitest": "4.1.11"
51
+ "vitest": "4.1.12"
52
52
  },
53
53
  "repository": {
54
54
  "type": "git",