@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 +59 -109
- package/dist/TeleCryptIOStorage.d.ts +2 -0
- package/dist/TeleCryptIOStorage.js +11 -8
- package/dist/core/operations.d.ts +1 -1
- package/dist/core/operations.js +24 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,19 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@telecrypt-io/storage)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
## Quick start
|
|
15
|
+
## Browser quick start
|
|
28
16
|
|
|
29
|
-
|
|
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
|
-
|
|
39
|
-
await core.
|
|
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
|
-
|
|
43
|
-
and
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
Apache License 2.0 on 2030-07-20.
|
|
79
|
+
## Source repositories
|
|
134
80
|
|
|
135
|
-
|
|
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
|
-
##
|
|
84
|
+
## License
|
|
138
85
|
|
|
139
|
-
|
|
140
|
-
|
|
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:
|
|
1040
|
-
state_default:
|
|
1041
|
-
events_default:
|
|
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]:
|
|
1049
|
-
[EventType.RoomMessage]:
|
|
1050
|
-
[EventType.RoomMessageEncrypted]:
|
|
1051
|
-
[EventType.Sticker]:
|
|
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
|
|
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[]>;
|
package/dist/core/operations.js
CHANGED
|
@@ -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
|
|
731
|
+
/** Invites `userId` to the vault as a reader. */
|
|
732
732
|
export async function shareVault(storage, vaultId, userId, role, options) {
|
|
733
|
-
if (role !== "viewer"
|
|
734
|
-
throw new StorageError("
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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,
|
|
1115
|
-
completedIds.push(
|
|
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)
|