@telecrypt-io/storage 0.7.1 → 0.7.2

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
@@ -2,19 +2,9 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@telecrypt-io/storage)](https://www.npmjs.com/package/@telecrypt-io/storage)
4
4
 
5
- End-to-end encrypted file storage and sharing, built on Matrix.
6
-
7
- The current public TeleCrypt architecture, responsibilities, and product limits are authoritative at
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
- and development workflow.
10
-
11
- Files are encrypted on the client before upload. The server stores only opaque ciphertext and
12
- never holds the decryption keys. Shared vaults let multiple people add and read files, and a
13
- Recovery Key restores your files on a new device — even if you lose the original.
14
-
15
- This repository publishes the library package from annotated version tags using npm Trusted
16
- Publishing. GitHub Releases identify the corresponding source version. The command-line client is
17
- maintained in its own repository.
5
+ A JavaScript/TypeScript SDK for encrypted Matrix file trees, based on
6
+ [MSC3089](https://github.com/matrix-org/matrix-spec-proposals/pull/3089), an open and currently
7
+ unmerged Matrix proposal.
18
8
 
19
9
  ## Install
20
10
 
@@ -22,119 +12,79 @@ maintained in its own repository.
22
12
  npm install @telecrypt-io/storage
23
13
  ```
24
14
 
25
- This gives you the `TeleCryptIOStorage` library and its browser-safe `core` API.
26
-
27
- ## Quick start
15
+ ## Browser quick start
28
16
 
29
- **Library:**
17
+ The example below starts with an authenticated OIDC session. Obtain `userId`, `accessToken`, and
18
+ `deviceId` from that completed session. `baseUrl` and `serverName` must come from trusted
19
+ application configuration for the same Matrix deployment; do not accept them from an untrusted
20
+ request.
30
21
 
31
22
  ```ts
32
23
  import { TeleCryptIOStorage } from "@telecrypt-io/storage";
33
24
  import * as core from "@telecrypt-io/storage/core";
34
25
 
26
+ // baseUrl/serverName come from trusted application configuration.
27
+ // userId/accessToken/deviceId come from completed OIDC authentication.
35
28
  const storage = await TeleCryptIOStorage.create({
36
- baseUrl, userId, accessToken, deviceId,
29
+ baseUrl, serverName, userId, accessToken, deviceId,
37
30
  });
38
- const vault = await core.createVault(storage, "Photos");
39
- await core.uploadFile(storage, vault.id, "cat.jpg", bytes, "image/jpeg");
31
+ try {
32
+ const vault = await core.createVault(storage, "Example");
33
+ const file = await core.uploadFile(
34
+ storage, vault.id, "hello.txt",
35
+ new TextEncoder().encode("Hello"), "text/plain",
36
+ );
37
+ const downloaded = await core.downloadFile(storage, vault.id, file.id);
38
+ console.log(new TextDecoder().decode(downloaded.bytes));
39
+ } finally {
40
+ storage.getClient().stopClient();
41
+ }
40
42
  ```
41
43
 
42
- The recommended constructors configure the Matrix client with the SDK's manual-redirect transport
43
- and finite request deadline. The public `new TeleCryptIOStorage(client)` constructor is
44
- an advanced escape hatch: it does not rewrite matrix-js-sdk internals. Callers using it must build
45
- the client through matrix-js-sdk's supported `createClient` options (`fetchFn` and
46
- `localTimeoutMs`) and remain responsible for the safety of other Matrix SDK requests. SDK methods
47
- that need the authenticated transport fail closed when it is unavailable; they do not silently
48
- install a private transport configuration.
49
-
50
- Same-name vault/folder creation is serialized within one `TeleCryptIOStorage` instance, but
51
- each call creates a distinct room. Display names are labels, not identities, and Matrix
52
- provides no server-side room-creation idempotency key.
53
-
54
- ## OIDC API
55
-
56
- The SDK owns the browser authorization-code PKCE context in `sessionStorage` and provides device-code,
57
- discovery, dynamic registration, identity confirmation, and refresh helpers. OAuth metadata, token
58
- scopes, Matrix user/device identities, redirects, and complete response bodies are validated before
59
- they are returned. Network-bound helpers accept `AbortSignal` where cancellation is meaningful; token
60
- refresh uses a public client and persists the resulting token pair through the caller's callback.
61
-
62
- The command-line client is sourced by
63
- [`TeleCrypt-io/storage-cli`](https://github.com/TeleCrypt-io/storage-cli). The static web
64
- application is sourced by
65
- [`TeleCrypt-io/storage.telecrypt.io`](https://github.com/TeleCrypt-io/storage.telecrypt.io).
66
-
67
- ### 0.5 OIDC migration
68
-
69
- Version 0.5 targets Matrix JS SDK 42 and the stable Matrix OAuth metadata endpoint. Existing
70
- integrations must use the current `createFromOidc` options and pass the confirmed Matrix user ID,
71
- device ID, and access token returned by the login flow. Replace old Matrix SDK OIDC imports and
72
- private HTTP discovery with `discoverOidcIssuer`, `registerClient`, the device-code or PKCE flow
73
- helpers, and `whoAmI` from `core/oidc`; the discovery URL is the stable
74
- `/_matrix/client/v1/auth_metadata` endpoint. Refresh integration uses
75
- `buildTokenRefreshFunction` as the current `tokenRefreshFunction` callback and must persist the
76
- returned token pair before reporting success. The SDK no longer carries pre-0.5 compatibility
77
- paths, so no adapter-side shim is required or supported. Discovery accepts providers that omit
78
- `revocation_endpoint`; revocation is only an opportunistic cleanup path after refresh persistence
79
- fails. Device-code sessions retain their requested device ID in-process; callers that reconstruct a
80
- session must pass its expected device ID to `waitForDeviceCodeLogin`. `buildTokenRefreshFunction`
81
- always requires the expected device ID because a reconstructed refresh callback has no safe implicit
82
- binding. Returned OAuth scopes are rejected unless they match that intended Matrix device exactly.
83
-
84
- ## How it works
85
-
86
- Built on [MSC3089](https://github.com/matrix-org/matrix-spec-proposals/pull/3089), which models
87
- a file tree using Matrix primitives:
88
-
89
- | Storage concept | Matrix concept |
90
- |---|---|
91
- | Vault | A Space (room marked as a file tree) |
92
- | Folder | A child Space |
93
- | File | An event pointing at encrypted uploaded content |
94
- | Sharing | Room invitation |
95
- | Permissions | Power levels |
96
-
97
- ### Deletion
98
-
99
- Vaults and folders are deleted only when they are empty. Delete each file first; deleting a file
100
- also removes its encrypted media object before redacting its Matrix event. Delete empty child
101
- folders before deleting their parent. A nonempty delete
102
- fails with `NonEmptyTreeError`, so a shared or nested tree is never removed implicitly.
103
-
104
- Encryption uses the same scheme as Matrix attachments (AES-CTR with a per-file key, keys
105
- distributed via the room's Megolm session). File deletion uses TeleCrypt's authenticated Synapse
106
- storage extension to remove the local media object before Matrix redaction; stock
107
- Synapse without that extension cannot provide the SDK deletion contract.
108
-
109
- ## Development
44
+ `TeleCryptIOStorage.create` initializes the Matrix client and its persistent browser crypto store
45
+ by default. Browser persistence uses IndexedDB and the Matrix Rust crypto WASM runtime, so this
46
+ example assumes a browser environment. OIDC discovery, login, refresh, and the corresponding
47
+ `createFromOidc` inputs are exported from `@telecrypt-io/storage/core`.
110
48
 
111
- ```bash
112
- npm ci --ignore-scripts --no-fund --no-audit
113
- npm run test:unit # unit-only checks; does not start Podman
114
- npm run synapse:up # disposable local Synapse/MAS fixture
115
- npm test # full SDK suite, including fixture-backed functional tests
116
- ```
49
+ The example creates retained Matrix rooms and encrypted media. Delete its file and then its empty
50
+ vault when finished; cleanup is part of the operation, not an automatic consequence of stopping
51
+ the client.
52
+
53
+ ## Storage model and behavior
54
+
55
+ The SDK maps the file tree to Matrix primitives defined by MSC3089:
117
56
 
118
- The full suite uses the real local Synapse/MAS/Postgres fixture in Podman at `localhost:8008`, never
119
- a production server. This is the shared fixture for the SDK, CLI, and Web functional/e2e suites;
120
- start it from this checkout and reuse it for the sibling repositories. Unit tests may use mocks for
121
- isolated boundaries, but they do not replace the real-stack checks.
57
+ - A vault is a Matrix Space room marked as a file tree.
58
+ - A folder is a child Space in that tree.
59
+ - A file is a Matrix event referring to encrypted uploaded content.
60
+ - Sharing uses Matrix room invitations and permissions use Matrix power levels.
122
61
 
123
- If setup or tests fail, inspect the relevant logs, then run `npm run synapse:down` when finished.
62
+ Vault, folder, and file IDs are opaque Matrix room and event IDs. Names are display labels: two
63
+ objects with the same name are still distinct, and a name is not an idempotency key.
124
64
 
125
- After a successful run, use `npm run synapse:down` for scoped teardown. Use
126
- `./throwaway_synapse/down.sh --wipe` only when a complete fixture reset is explicitly required.
65
+ File bytes are encrypted in the client before upload with Matrix attachment encryption (AES-CTR
66
+ and a per-file key distributed through the room's Megolm session). The server stores encrypted
67
+ media and Matrix events, but encryption does not hide all room, event, or other metadata from the
68
+ server or users who can read the room.
127
69
 
128
- See [RELEASING.md](./RELEASING.md) for the npm release procedure.
70
+ Recovery is explicit. Use `core.setupRecovery(storage)` to create a recovery key and keep that key
71
+ securely; use `core.restoreRecovery(storage, recoveryKey)` on a new device. Losing the recovery key
72
+ can prevent encrypted keys from being restored.
129
73
 
130
- ## Licence
74
+ Deletion has strict ordering. Delete files before their folders or vaults because folders and
75
+ vaults must be empty. File deletion removes the encrypted media object before redacting its Matrix
76
+ event and requires TeleCrypt's Synapse storage extension; stock Synapse cannot provide that
77
+ deletion contract.
131
78
 
132
- [Business Source License 1.1](./LICENSE). Non-commercial use is permitted; converts to
133
- Apache License 2.0 on 2030-07-20.
79
+ ## Source repositories
134
80
 
135
- For commercial licensing, contact TeleCrypt.io.
81
+ - Web application: [`TeleCrypt-io/storage`](https://github.com/TeleCrypt-io/storage)
82
+ - Command-line client: [`cli/`](https://github.com/TeleCrypt-io/storage/tree/main/cli)
136
83
 
137
- ## Third-party code
84
+ ## License
138
85
 
139
- - [`matrix-js-sdk`](https://github.com/matrix-org/matrix-js-sdk) — Apache-2.0 — dependency
140
- - [`matrix-encrypt-attachment`](https://github.com/matrix-org/matrix-encrypt-attachment) — Apache-2.0 — dependency
86
+ See [`LICENSE`](./LICENSE) for the complete terms. The current license is Business Source License
87
+ 1.1 with an additional grant for non-commercial use; it is not an open-source license. The stated
88
+ Change Date is 2030-07-20, and the Change License is Apache License 2.0. The license also changes
89
+ on the fourth anniversary of the first public distribution of a specific version if that occurs
90
+ before its stated Change Date. The full license controls in case of any discrepancy.
@@ -26,6 +26,8 @@ export interface TreeSpace {
26
26
  }
27
27
  export interface FileBranch {
28
28
  readonly id: string;
29
+ /** Current MSC3089 branch/listing event. Its event ID is distinct from `id` (the state key). */
30
+ readonly indexEvent?: Pick<MatrixEvent, "getId">;
29
31
  /** Matrix SDK keeps a redacted branch in state as an inactive object. */
30
32
  readonly isActive?: boolean;
31
33
  getName(): string;
@@ -1,4 +1,4 @@
1
- import { ClientEvent, createClient, EventType, MatrixEvent, MatrixError, Preset, RoomCreateTypeField, RoomType, SyncState, UNSTABLE_MSC3088_ENABLED, UNSTABLE_MSC3088_PURPOSE, UNSTABLE_MSC3089_TREE_SUBTYPE, } from "matrix-js-sdk";
1
+ import { ClientEvent, createClient, EventType, MatrixEvent, MatrixError, Preset, RoomCreateTypeField, RoomType, SyncState, UNSTABLE_MSC3088_ENABLED, UNSTABLE_MSC3088_PURPOSE, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_TREE_SUBTYPE, } from "matrix-js-sdk";
2
2
  import { Method } from "matrix-js-sdk/lib/http-api/method.js";
3
3
  import { ClientPrefix } from "matrix-js-sdk/lib/http-api/prefix.js";
4
4
  import { encryptAttachment, decryptAttachment } from "matrix-encrypt-attachment";
@@ -1036,19 +1036,22 @@ export class TeleCryptIOStorage {
1036
1036
  invite: 100,
1037
1037
  kick: 100,
1038
1038
  ban: 100,
1039
- redact: 50,
1040
- state_default: 50,
1041
- events_default: 50,
1039
+ redact: 100,
1040
+ state_default: 0,
1041
+ events_default: 0,
1042
1042
  users_default: 0,
1043
1043
  events: {
1044
1044
  [EventType.RoomPowerLevels]: 100,
1045
1045
  [EventType.RoomHistoryVisibility]: 100,
1046
1046
  [EventType.RoomTombstone]: 100,
1047
1047
  [EventType.RoomEncryption]: 100,
1048
- [EventType.RoomName]: 50,
1049
- [EventType.RoomMessage]: 50,
1050
- [EventType.RoomMessageEncrypted]: 50,
1051
- [EventType.Sticker]: 50,
1048
+ [EventType.RoomName]: 100,
1049
+ [EventType.RoomMessage]: 0,
1050
+ [EventType.RoomMessageEncrypted]: 0,
1051
+ [EventType.Sticker]: 0,
1052
+ [EventType.SpaceChild]: 100,
1053
+ [EventType.SpaceParent]: 100,
1054
+ [UNSTABLE_MSC3089_BRANCH.name]: 100,
1052
1055
  },
1053
1056
  users: { [userId]: 100 },
1054
1057
  },
@@ -31,7 +31,7 @@ export declare function declineInvite(storage: TeleCryptIOStorage, vaultId: stri
31
31
  vaultId: string;
32
32
  declined: boolean;
33
33
  }>;
34
- /** Invites `userId` to the vault and applies the requested role. */
34
+ /** Invites `userId` to the vault as a reader. */
35
35
  export declare function shareVault(storage: TeleCryptIOStorage, vaultId: string, userId: string, role: string, options?: OperationOptions): Promise<ShareResult>;
36
36
  export declare function unshareVault(storage: TeleCryptIOStorage, vaultId: string, userId: string, options?: OperationOptions): Promise<UnshareResult>;
37
37
  export declare function listMembers(storage: TeleCryptIOStorage, vaultId: string, options?: OperationOptions): Promise<Member[]>;
@@ -728,10 +728,10 @@ export async function declineInvite(storage, vaultId, options) {
728
728
  return { vaultId, declined: true };
729
729
  }, "mutation");
730
730
  }
731
- /** Invites `userId` to the vault and applies the requested role. */
731
+ /** Invites `userId` to the vault as a reader. */
732
732
  export async function shareVault(storage, vaultId, userId, role, options) {
733
- if (role !== "viewer" && role !== "editor") {
734
- throw new StorageError("invalid --role (must be viewer or editor)");
733
+ if (role !== "viewer") {
734
+ throw new StorageError("storage sharing supports viewers only");
735
735
  }
736
736
  const operation = createOperationDeadline(options);
737
737
  const completedRoomIds = new Set();
@@ -754,6 +754,12 @@ export async function shareVault(storage, vaultId, userId, role, options) {
754
754
  }
755
755
  for (const [index, space] of spaces.entries()) {
756
756
  ensureOperationActive(operation.signal);
757
+ const members = index === 0
758
+ ? currentMembers
759
+ : await withRateLimitRetry(() => storage.listMembers(space, { signal: operation.signal }), operation.signal);
760
+ if (members.some((member) => member.userId === userId && member.role === "owner")) {
761
+ throw new StorageError("share will not demote an existing owner");
762
+ }
757
763
  const currentMembership = index === 0
758
764
  ? currentMembers.find((member) => member.userId === userId)?.membership
759
765
  : await storage.getRoomMembership(space.id, userId, {
@@ -778,17 +784,10 @@ export async function shareVault(storage, vaultId, userId, role, options) {
778
784
  throw new StorageError("share failed");
779
785
  }
780
786
  }
781
- for (const space of spaces) {
782
- ensureOperationActive(operation.signal);
783
- const members = space.id === tree.id
784
- ? currentMembers
785
- : await withRateLimitRetry(() => storage.listMembers(space, { signal: operation.signal }), operation.signal);
786
- if (members.some((member) => member.userId === userId && member.role === "owner")) {
787
- throw new StorageError("share will not demote an existing owner");
788
- }
789
- await withRateLimitRetry(() => withMatrixMutationAbort(() => space.setPermissions(userId, role), operation.signal), operation.signal);
790
- completedRoomIds.add(space.id);
791
- }
787
+ // Storage rooms have one immutable owner power level and readers use
788
+ // the room's default user level. Inviting is the complete sharing
789
+ // operation; changing per-user power levels would create editors and
790
+ // let metadata authorship drift away from the owner.
792
791
  }
793
792
  catch (error) {
794
793
  if (error instanceof MutationOutcomeUnknownError)
@@ -805,7 +804,7 @@ export async function shareVault(storage, vaultId, userId, role, options) {
805
804
  }
806
805
  throw new StorageError("share failed", { cause: error });
807
806
  }
808
- return { vaultId, userId, role };
807
+ return { vaultId, userId, role: "viewer" };
809
808
  }, operation.signal);
810
809
  return await raceOperationDeadline(operation, pending, "mutation");
811
810
  }
@@ -1110,9 +1109,17 @@ export async function deleteFile(storage, treeId, fileId, options) {
1110
1109
  const completedIds = [];
1111
1110
  try {
1112
1111
  const client = storage.getClient();
1112
+ // MSC3089 uses the file event ID as the branch state key, but the current
1113
+ // listing is a separate state event. Redact that current listing after
1114
+ // clearing the branch state; redacting only the file event leaves the
1115
+ // listing visible and is especially harmful when a user is suspended.
1116
+ const listingEventId = branch.indexEvent?.getId?.() ?? branch.id;
1117
+ if (typeof listingEventId !== "string" || listingEventId.length === 0) {
1118
+ throw new StorageError("encrypted file has no listing event identifier");
1119
+ }
1113
1120
  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);
1115
- completedIds.push(branch.id);
1121
+ await withRateLimitRetry(() => withMatrixMutationAbort(() => client.redactEvent(tree.id, listingEventId), signal, "delete file event"), signal);
1122
+ completedIds.push(listingEventId);
1116
1123
  }
1117
1124
  catch (error) {
1118
1125
  if (error instanceof MutationOutcomeUnknownError)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telecrypt-io/storage",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "license": "BUSL-1.1",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.19.0",