@telecrypt-io/storage 0.2.13 → 0.4.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 +12 -13
- package/dist/TeleCryptIOStorage.d.ts +3 -3
- package/dist/TeleCryptIOStorage.js +4 -5
- package/dist/core/errors.d.ts +3 -4
- package/dist/core/errors.js +4 -5
- package/dist/core/index.d.ts +1 -2
- package/dist/core/index.js +1 -2
- package/dist/core/oidc.d.ts +2 -4
- package/dist/core/oidc.js +10 -12
- package/dist/core/operations.d.ts +36 -27
- package/dist/core/operations.js +104 -79
- package/dist/core/types.d.ts +16 -12
- package/dist/core/types.js +1 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,17 +5,16 @@
|
|
|
5
5
|
End-to-end encrypted file storage and sharing, built on Matrix.
|
|
6
6
|
|
|
7
7
|
Files are encrypted on the client before upload. The server stores only opaque ciphertext and
|
|
8
|
-
never holds the decryption keys. Shared
|
|
8
|
+
never holds the decryption keys. Shared vaults let multiple people add and read files, and a
|
|
9
9
|
Recovery Key restores your files on a new device — even if you lose the original.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
provenance. The older `0.1.3` release is the immutable legacy combined library-and-CLI package.
|
|
11
|
+
This repository publishes the library package from protected exact-version tags with NPM Trusted
|
|
12
|
+
Publishing/OIDC provenance. The command-line client is maintained in its own repository.
|
|
14
13
|
|
|
15
14
|
## Install
|
|
16
15
|
|
|
17
16
|
```bash
|
|
18
|
-
npm install @telecrypt-io/storage@0.
|
|
17
|
+
npm install @telecrypt-io/storage@0.4.0
|
|
19
18
|
```
|
|
20
19
|
|
|
21
20
|
This gives you the `TeleCryptIOStorage` library and its browser-safe `core` API.
|
|
@@ -31,8 +30,8 @@ import * as core from "@telecrypt-io/storage/core";
|
|
|
31
30
|
const storage = await TeleCryptIOStorage.create({
|
|
32
31
|
baseUrl, userId, accessToken, deviceId,
|
|
33
32
|
});
|
|
34
|
-
const
|
|
35
|
-
await core.uploadFile(storage,
|
|
33
|
+
const vault = await core.createVault(storage, "Photos");
|
|
34
|
+
await core.uploadFile(storage, vault.id, "cat.jpg", bytes, "image/jpeg");
|
|
36
35
|
```
|
|
37
36
|
|
|
38
37
|
The command-line client is sourced by
|
|
@@ -45,10 +44,10 @@ application is sourced by
|
|
|
45
44
|
Built on [MSC3089](https://github.com/matrix-org/matrix-spec-proposals/pull/3089), which models
|
|
46
45
|
a file tree using Matrix primitives:
|
|
47
46
|
|
|
48
|
-
|
|
|
47
|
+
| Storage concept | Matrix concept |
|
|
49
48
|
|---|---|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
49
|
+
| Vault | A Space (room marked as a file tree) |
|
|
50
|
+
| Folder | A child Space |
|
|
52
51
|
| File | An event pointing at encrypted uploaded content |
|
|
53
52
|
| Version | A newer event superseding the old |
|
|
54
53
|
| Sharing | Room invitation |
|
|
@@ -61,13 +60,13 @@ stock Synapse.
|
|
|
61
60
|
## Development
|
|
62
61
|
|
|
63
62
|
```bash
|
|
64
|
-
npm
|
|
65
|
-
npm run synapse:up # disposable local Synapse
|
|
63
|
+
npm ci
|
|
64
|
+
npm run synapse:up # disposable local Synapse/MAS fixture
|
|
66
65
|
npm test
|
|
67
66
|
npm run synapse:down
|
|
68
67
|
```
|
|
69
68
|
|
|
70
|
-
Tests run against a real local Synapse in
|
|
69
|
+
Tests run against a real local Synapse/MAS fixture in Podman, never against a production server.
|
|
71
70
|
|
|
72
71
|
See [RELEASING.md](./RELEASING.md) for the guarded npm release procedure.
|
|
73
72
|
|
|
@@ -194,7 +194,7 @@ export declare class TeleCryptIOStorage {
|
|
|
194
194
|
}>;
|
|
195
195
|
};
|
|
196
196
|
/**
|
|
197
|
-
* Creates a new top-level shared
|
|
197
|
+
* Creates a new top-level shared vault (tree space).
|
|
198
198
|
*
|
|
199
199
|
* Works around a genuine race in matrix-js-sdk's own
|
|
200
200
|
* `unstableCreateFileTree()`: it creates the room via a plain
|
|
@@ -209,7 +209,7 @@ export declare class TeleCryptIOStorage {
|
|
|
209
209
|
* how we get "Unknown room" at all); this method waits for the client's
|
|
210
210
|
* own ongoing sync loop to surface it, then wraps it directly via
|
|
211
211
|
* `unstableGetFileTreeSpace`, instead of surfacing a spurious failure for
|
|
212
|
-
* a
|
|
212
|
+
* a vault that in fact exists.
|
|
213
213
|
*/
|
|
214
214
|
createTree(name: string): Promise<TreeSpace>;
|
|
215
215
|
listTrees(): Promise<TreeSpace[]>;
|
|
@@ -230,7 +230,7 @@ export declare class TeleCryptIOStorage {
|
|
|
230
230
|
createSubtree(parent: TreeSpace, name: string): Promise<TreeSpace>;
|
|
231
231
|
getTree(roomId: string): TreeSpace | null;
|
|
232
232
|
/**
|
|
233
|
-
* Lists the participants of a shared
|
|
233
|
+
* Lists the participants of a shared vault (tree space) and their role,
|
|
234
234
|
* derived from room membership (join/invite state) + power levels.
|
|
235
235
|
* Includes invited-but-not-yet-joined members as well as joined ones;
|
|
236
236
|
* excludes anyone who has left/been kicked/banned.
|
|
@@ -250,7 +250,7 @@ export class TeleCryptIOStorage {
|
|
|
250
250
|
};
|
|
251
251
|
}
|
|
252
252
|
/**
|
|
253
|
-
* Creates a new top-level shared
|
|
253
|
+
* Creates a new top-level shared vault (tree space).
|
|
254
254
|
*
|
|
255
255
|
* Works around a genuine race in matrix-js-sdk's own
|
|
256
256
|
* `unstableCreateFileTree()`: it creates the room via a plain
|
|
@@ -265,7 +265,7 @@ export class TeleCryptIOStorage {
|
|
|
265
265
|
* how we get "Unknown room" at all); this method waits for the client's
|
|
266
266
|
* own ongoing sync loop to surface it, then wraps it directly via
|
|
267
267
|
* `unstableGetFileTreeSpace`, instead of surfacing a spurious failure for
|
|
268
|
-
* a
|
|
268
|
+
* a vault that in fact exists.
|
|
269
269
|
*/
|
|
270
270
|
async createTree(name) {
|
|
271
271
|
const knownRoomIdsBefore = new Set(this.client.getRooms().map((r) => r.roomId));
|
|
@@ -369,7 +369,7 @@ export class TeleCryptIOStorage {
|
|
|
369
369
|
return this.client.unstableGetFileTreeSpace(roomId);
|
|
370
370
|
}
|
|
371
371
|
/**
|
|
372
|
-
* Lists the participants of a shared
|
|
372
|
+
* Lists the participants of a shared vault (tree space) and their role,
|
|
373
373
|
* derived from room membership (join/invite state) + power levels.
|
|
374
374
|
* Includes invited-but-not-yet-joined members as well as joined ones;
|
|
375
375
|
* excludes anyone who has left/been kicked/banned.
|
|
@@ -434,8 +434,7 @@ export class TeleCryptIOStorage {
|
|
|
434
434
|
// of a useful error. Surface the real cause.
|
|
435
435
|
throw new Error(`downloadFile: could not read file info from the event — it is likely undecryptable on this device (missing megolm session; try restoring from a Recovery Key)`);
|
|
436
436
|
}
|
|
437
|
-
//
|
|
438
|
-
// placeholder without throwing, still fail with the clear message.
|
|
437
|
+
// Also reject an incomplete placeholder if matrix-js-sdk returns one.
|
|
439
438
|
if (!info || typeof info.url !== "string") {
|
|
440
439
|
throw new Error("downloadFile: could not read file info from the event — it is likely undecryptable on this device (missing megolm session; try restoring from a Recovery Key)");
|
|
441
440
|
}
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/** A user-facing operation error: a clean, prefix-free message meant to be
|
|
2
|
-
* shown directly to a user
|
|
3
|
-
*
|
|
4
|
-
* conditions (bad login, wrong recovery key, missing file/folder, not
|
|
2
|
+
* shown directly to a user — never a raw stack trace. Thrown for expected failure
|
|
3
|
+
* conditions (bad login, wrong recovery key, missing file, vault, or folder, not
|
|
5
4
|
* logged in, ...). Platform-agnostic: no Node/CLI dependencies. */
|
|
6
|
-
export declare class
|
|
5
|
+
export declare class StorageError extends Error {
|
|
7
6
|
constructor(message: string);
|
|
8
7
|
}
|
package/dist/core/errors.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/** A user-facing operation error: a clean, prefix-free message meant to be
|
|
2
|
-
* shown directly to a user
|
|
3
|
-
*
|
|
4
|
-
* conditions (bad login, wrong recovery key, missing file/folder, not
|
|
2
|
+
* shown directly to a user — never a raw stack trace. Thrown for expected failure
|
|
3
|
+
* conditions (bad login, wrong recovery key, missing file, vault, or folder, not
|
|
5
4
|
* logged in, ...). Platform-agnostic: no Node/CLI dependencies. */
|
|
6
|
-
export class
|
|
5
|
+
export class StorageError extends Error {
|
|
7
6
|
constructor(message) {
|
|
8
7
|
super(message);
|
|
9
|
-
this.name = "
|
|
8
|
+
this.name = "StorageError";
|
|
10
9
|
}
|
|
11
10
|
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* needs — the typed result contract plus every operation. */
|
|
1
|
+
/** The full platform-agnostic surface for user-facing clients. */
|
|
3
2
|
export * from "./types.js";
|
|
4
3
|
export * from "./operations.js";
|
|
5
4
|
export * from "./errors.js";
|
package/dist/core/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* needs — the typed result contract plus every operation. */
|
|
1
|
+
/** The full platform-agnostic surface for user-facing clients. */
|
|
3
2
|
export * from "./types.js";
|
|
4
3
|
export * from "./operations.js";
|
|
5
4
|
export * from "./errors.js";
|
package/dist/core/oidc.d.ts
CHANGED
|
@@ -21,10 +21,8 @@ export interface OidcTokenSet {
|
|
|
21
21
|
expiresAt?: number;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* Discovers the OIDC/MAS issuer
|
|
25
|
-
*
|
|
26
|
-
* the legacy `/auth_issuer` + issuer well-known) — the non-deprecated path
|
|
27
|
-
* recommended over `discoverAndValidateOIDCIssuerWellKnown`.
|
|
24
|
+
* Discovers the OIDC/MAS issuer and endpoints for a homeserver via the
|
|
25
|
+
* Matrix client's authentication metadata endpoint.
|
|
28
26
|
*
|
|
29
27
|
* NODE CALLERS: this internally constructs oidc-client-ts state that
|
|
30
28
|
* requires `window.sessionStorage`/`window.localStorage` to exist (a real
|
package/dist/core/oidc.js
CHANGED
|
@@ -11,12 +11,10 @@
|
|
|
11
11
|
import { createClient } from "matrix-js-sdk";
|
|
12
12
|
import { registerOidcClient, generateScope, generateOidcAuthorizationUrl, completeAuthorizationCodeGrant, startDeviceAuthorization, waitForDeviceAuthorization, } from "matrix-js-sdk/lib/oidc/index.js";
|
|
13
13
|
import { OidcClient, WebStorageStateStore } from "oidc-client-ts";
|
|
14
|
-
import {
|
|
14
|
+
import { StorageError } from "./errors.js";
|
|
15
15
|
/**
|
|
16
|
-
* Discovers the OIDC/MAS issuer
|
|
17
|
-
*
|
|
18
|
-
* the legacy `/auth_issuer` + issuer well-known) — the non-deprecated path
|
|
19
|
-
* recommended over `discoverAndValidateOIDCIssuerWellKnown`.
|
|
16
|
+
* Discovers the OIDC/MAS issuer and endpoints for a homeserver via the
|
|
17
|
+
* Matrix client's authentication metadata endpoint.
|
|
20
18
|
*
|
|
21
19
|
* NODE CALLERS: this internally constructs oidc-client-ts state that
|
|
22
20
|
* requires `window.sessionStorage`/`window.localStorage` to exist (a real
|
|
@@ -31,7 +29,7 @@ export async function discoverOidcIssuer(homeserverBaseUrl) {
|
|
|
31
29
|
return await client.getAuthMetadata();
|
|
32
30
|
}
|
|
33
31
|
catch (err) {
|
|
34
|
-
throw new
|
|
32
|
+
throw new StorageError(`OIDC discovery failed: ${err.message}`);
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
35
|
/**
|
|
@@ -44,7 +42,7 @@ export async function registerClient(authMetadata, metadata) {
|
|
|
44
42
|
return await registerOidcClient(authMetadata, metadata);
|
|
45
43
|
}
|
|
46
44
|
catch (err) {
|
|
47
|
-
throw new
|
|
45
|
+
throw new StorageError(`OIDC dynamic client registration failed: ${err.message}`);
|
|
48
46
|
}
|
|
49
47
|
}
|
|
50
48
|
// ---------------------------------------------------------------------------
|
|
@@ -177,26 +175,26 @@ export async function refreshOidcToken(tokenEndpoint, clientId, refreshToken) {
|
|
|
177
175
|
});
|
|
178
176
|
if (!res.ok) {
|
|
179
177
|
const body = await res.text();
|
|
180
|
-
throw new
|
|
178
|
+
throw new StorageError(`OIDC token refresh failed (${res.status}): ${body}`);
|
|
181
179
|
}
|
|
182
180
|
const data = await res.json();
|
|
183
181
|
if (!data || typeof data !== "object") {
|
|
184
|
-
throw new
|
|
182
|
+
throw new StorageError("OIDC token refresh returned an invalid response");
|
|
185
183
|
}
|
|
186
184
|
const record = data;
|
|
187
185
|
const accessToken = record.access_token;
|
|
188
186
|
const nextRefreshToken = record.refresh_token;
|
|
189
187
|
const expiresIn = record.expires_in;
|
|
190
188
|
if (typeof accessToken !== "string" || accessToken.trim() === "") {
|
|
191
|
-
throw new
|
|
189
|
+
throw new StorageError("OIDC token refresh returned no access token");
|
|
192
190
|
}
|
|
193
191
|
if (nextRefreshToken !== undefined &&
|
|
194
192
|
(typeof nextRefreshToken !== "string" || nextRefreshToken.trim() === "")) {
|
|
195
|
-
throw new
|
|
193
|
+
throw new StorageError("OIDC token refresh returned an invalid refresh token");
|
|
196
194
|
}
|
|
197
195
|
if (expiresIn !== undefined &&
|
|
198
196
|
(typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn < 0)) {
|
|
199
|
-
throw new
|
|
197
|
+
throw new StorageError("OIDC token refresh returned an invalid expiry");
|
|
200
198
|
}
|
|
201
199
|
return {
|
|
202
200
|
accessToken,
|
|
@@ -3,50 +3,59 @@
|
|
|
3
3
|
* taking an already-created `TeleCryptIOStorage` plus plain inputs and returning
|
|
4
4
|
* one of the typed results in `./types.ts`. No I/O beyond the Matrix client
|
|
5
5
|
* itself, no stdout, no `process`, no file paths — bytes in/out are always
|
|
6
|
-
* `Uint8Array`.
|
|
7
|
-
*
|
|
8
|
-
* and share the exact same result shapes.
|
|
6
|
+
* `Uint8Array`. All callers run the same tested logic and share the same result
|
|
7
|
+
* shapes.
|
|
9
8
|
*
|
|
10
9
|
* `core/` never creates the `TeleCryptIOStorage`/`MatrixClient` itself — store
|
|
11
10
|
* config (persistent crypto store, session credentials, etc.) is
|
|
12
|
-
* platform-specific and stays with the caller
|
|
11
|
+
* platform-specific and stays with the caller.
|
|
13
12
|
*/
|
|
14
13
|
import { TeleCryptIOStorage } from "../TeleCryptIOStorage.js";
|
|
15
|
-
import type { DownloadedFile, FileDetails, FileInfo,
|
|
16
|
-
export declare function
|
|
17
|
-
/** Top-level
|
|
18
|
-
export declare function
|
|
19
|
-
/** Current account's effective role for a
|
|
20
|
-
export declare function
|
|
21
|
-
export declare function
|
|
14
|
+
import type { DownloadedFile, FolderDetails, FolderInfo, FileDetails, FileInfo, VaultDetails, VaultInfo, DeleteResult, JoinResult, Member, RecoveryRestore, RecoverySetup, RenameResult, ShareResult, UnshareResult } from "./types.js";
|
|
15
|
+
export declare function createVault(storage: TeleCryptIOStorage, name: string): Promise<VaultInfo>;
|
|
16
|
+
/** Top-level vaults only — excludes subdirectories of an existing tree. */
|
|
17
|
+
export declare function listVaults(storage: TeleCryptIOStorage): Promise<VaultInfo[]>;
|
|
18
|
+
/** Current account's effective role for a vault, or null while it is unavailable. */
|
|
19
|
+
export declare function getMyVaultRole(storage: TeleCryptIOStorage, vaultId: string): string | null;
|
|
20
|
+
export declare function joinVault(storage: TeleCryptIOStorage, vaultId: string): Promise<JoinResult>;
|
|
22
21
|
/** Rooms where this account is invited and the room looks like a file tree. */
|
|
23
|
-
export declare function listPendingInvites(storage: TeleCryptIOStorage): Promise<
|
|
24
|
-
/** Decline a
|
|
25
|
-
export declare function declineInvite(storage: TeleCryptIOStorage,
|
|
26
|
-
|
|
22
|
+
export declare function listPendingInvites(storage: TeleCryptIOStorage): Promise<VaultInfo[]>;
|
|
23
|
+
/** Decline a vault invite (same as leaving before join). */
|
|
24
|
+
export declare function declineInvite(storage: TeleCryptIOStorage, vaultId: string): Promise<{
|
|
25
|
+
vaultId: string;
|
|
27
26
|
declined: boolean;
|
|
28
27
|
}>;
|
|
29
28
|
/**
|
|
30
|
-
* Invites `userId` to the
|
|
29
|
+
* Invites `userId` to the vault at `role` and applies the role's
|
|
31
30
|
* permissions. Doubles as "change an existing participant's role" (call
|
|
32
31
|
* again with a different role): inviting someone who's already a member is
|
|
33
32
|
* a 403 from the server, not a real failure — that specific error is
|
|
34
33
|
* swallowed and the role change still applies. Any other invite failure
|
|
35
34
|
* (e.g. unknown user) propagates.
|
|
36
35
|
*/
|
|
37
|
-
export declare function
|
|
38
|
-
export declare function
|
|
39
|
-
export declare function listMembers(storage: TeleCryptIOStorage,
|
|
40
|
-
export declare function listFiles(storage: TeleCryptIOStorage,
|
|
41
|
-
|
|
42
|
-
export declare function
|
|
36
|
+
export declare function shareVault(storage: TeleCryptIOStorage, vaultId: string, userId: string, role: string): Promise<ShareResult>;
|
|
37
|
+
export declare function unshareVault(storage: TeleCryptIOStorage, vaultId: string, userId: string): Promise<UnshareResult>;
|
|
38
|
+
export declare function listMembers(storage: TeleCryptIOStorage, vaultId: string): Promise<Member[]>;
|
|
39
|
+
export declare function listFiles(storage: TeleCryptIOStorage, treeId: string): Promise<FileInfo[]>;
|
|
40
|
+
/** Lists the direct child folders of a vault or folder. */
|
|
41
|
+
export declare function listSubfolders(storage: TeleCryptIOStorage, parentId: string): Promise<FolderInfo[]>;
|
|
42
|
+
export declare function createSubfolder(storage: TeleCryptIOStorage, parentId: string, name: string): Promise<FolderInfo>;
|
|
43
|
+
/** Renames a top-level Vault. */
|
|
44
|
+
export declare function renameVault(storage: TeleCryptIOStorage, vaultId: string, name: string): Promise<RenameResult>;
|
|
45
|
+
/** Renames a nested folder. */
|
|
43
46
|
export declare function renameFolder(storage: TeleCryptIOStorage, folderId: string, name: string): Promise<RenameResult>;
|
|
47
|
+
/** Deletes a top-level Vault. */
|
|
48
|
+
export declare function deleteVault(storage: TeleCryptIOStorage, vaultId: string): Promise<DeleteResult>;
|
|
49
|
+
/** Deletes a nested folder. */
|
|
44
50
|
export declare function deleteFolder(storage: TeleCryptIOStorage, folderId: string): Promise<DeleteResult>;
|
|
45
|
-
export declare function renameFile(storage: TeleCryptIOStorage,
|
|
46
|
-
export declare function deleteFile(storage: TeleCryptIOStorage,
|
|
47
|
-
export declare function uploadFile(storage: TeleCryptIOStorage,
|
|
48
|
-
export declare function downloadFile(storage: TeleCryptIOStorage,
|
|
51
|
+
export declare function renameFile(storage: TeleCryptIOStorage, treeId: string, fileId: string, name: string): Promise<RenameResult>;
|
|
52
|
+
export declare function deleteFile(storage: TeleCryptIOStorage, treeId: string, fileId: string): Promise<DeleteResult>;
|
|
53
|
+
export declare function uploadFile(storage: TeleCryptIOStorage, treeId: string, name: string, bytes: Uint8Array, mimetype: string): Promise<FileInfo>;
|
|
54
|
+
export declare function downloadFile(storage: TeleCryptIOStorage, treeId: string, fileId: string): Promise<DownloadedFile>;
|
|
49
55
|
export declare function setupRecovery(storage: TeleCryptIOStorage): Promise<RecoverySetup>;
|
|
50
56
|
export declare function restoreRecovery(storage: TeleCryptIOStorage, recoveryKey: string): Promise<RecoveryRestore>;
|
|
51
|
-
export declare function getFileDetails(storage: TeleCryptIOStorage,
|
|
57
|
+
export declare function getFileDetails(storage: TeleCryptIOStorage, treeId: string, fileId: string): Promise<FileDetails>;
|
|
58
|
+
/** Returns details for a top-level Vault. */
|
|
59
|
+
export declare function getVaultDetails(storage: TeleCryptIOStorage, vaultId: string): Promise<VaultDetails>;
|
|
60
|
+
/** Returns details for a nested folder. */
|
|
52
61
|
export declare function getFolderDetails(storage: TeleCryptIOStorage, folderId: string): Promise<FolderDetails>;
|
package/dist/core/operations.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { StorageError } from "./errors.js";
|
|
2
2
|
import { waitForCondition } from "./poll.js";
|
|
3
3
|
/**
|
|
4
4
|
* Production Synapse enforces the built-in rc_messages budget (per-account
|
|
@@ -17,7 +17,7 @@ function isRateLimited(error) {
|
|
|
17
17
|
const isRateLimitError = error.isRateLimitError;
|
|
18
18
|
return typeof isRateLimitError === "function" && isRateLimitError.call(error) === true;
|
|
19
19
|
}
|
|
20
|
-
async function withRateLimitRetry(
|
|
20
|
+
async function withRateLimitRetry(operation) {
|
|
21
21
|
for (let attempt = 0;; attempt++) {
|
|
22
22
|
try {
|
|
23
23
|
return await operation();
|
|
@@ -43,61 +43,61 @@ async function withRateLimitRetry(label, operation) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
|
-
* Resolves a folder by ID, polling briefly: a room this same account just
|
|
46
|
+
* Resolves a vault or folder by ID, polling briefly: a room this same account just
|
|
47
47
|
* created (or was just invited to, by another process/session) can be
|
|
48
48
|
* momentarily absent from a from-scratch `/sync` before showing up moments
|
|
49
49
|
* later — real async settling, not an instant "not found". Throws a clean
|
|
50
|
-
* error if the
|
|
50
|
+
* error if the storage tree still isn't visible once the poll times out.
|
|
51
51
|
*/
|
|
52
|
-
async function resolveTree(storage,
|
|
52
|
+
async function resolveTree(storage, treeId) {
|
|
53
53
|
try {
|
|
54
|
-
return await waitForCondition(() => storage.getTree(
|
|
54
|
+
return await waitForCondition(() => storage.getTree(treeId), {
|
|
55
55
|
timeoutMs: 15000,
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
catch {
|
|
59
|
-
throw new
|
|
59
|
+
throw new StorageError(`storage space not found: ${treeId}`);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
/** As `resolveTree`, but for a specific file within an already-resolved
|
|
63
|
-
* folder — covers the same settling window for a file another
|
|
63
|
+
* vault or folder — covers the same settling window for a file another
|
|
64
64
|
* process/session just uploaded. */
|
|
65
65
|
async function resolveFile(tree, fileId) {
|
|
66
66
|
try {
|
|
67
67
|
return await waitForCondition(() => tree.getFile(fileId), { timeoutMs: 15000 });
|
|
68
68
|
}
|
|
69
69
|
catch {
|
|
70
|
-
throw new
|
|
70
|
+
throw new StorageError(`file not found: ${fileId}`);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
function toArrayBuffer(bytes) {
|
|
74
74
|
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
75
75
|
}
|
|
76
|
-
export async function
|
|
77
|
-
const tree = await withRateLimitRetry(
|
|
76
|
+
export async function createVault(storage, name) {
|
|
77
|
+
const tree = await withRateLimitRetry(() => storage.createTree(name));
|
|
78
78
|
return { id: tree.id, name };
|
|
79
79
|
}
|
|
80
|
-
/** Top-level
|
|
81
|
-
export async function
|
|
80
|
+
/** Top-level vaults only — excludes subdirectories of an existing tree. */
|
|
81
|
+
export async function listVaults(storage) {
|
|
82
82
|
const trees = await storage.listTrees();
|
|
83
83
|
return trees.filter((t) => t.isTopLevel).map((t) => ({ id: t.id, name: t.room.name }));
|
|
84
84
|
}
|
|
85
|
-
/** Current account's effective role for a
|
|
86
|
-
export function
|
|
85
|
+
/** Current account's effective role for a vault, or null while it is unavailable. */
|
|
86
|
+
export function getMyVaultRole(storage, vaultId) {
|
|
87
87
|
const userId = storage.getClient().getUserId();
|
|
88
|
-
const tree = storage.getTree(
|
|
88
|
+
const tree = storage.getTree(vaultId);
|
|
89
89
|
if (!userId || !tree)
|
|
90
90
|
return null;
|
|
91
91
|
return tree.getPermissions(userId);
|
|
92
92
|
}
|
|
93
|
-
export async function
|
|
93
|
+
export async function joinVault(storage, vaultId) {
|
|
94
94
|
try {
|
|
95
|
-
await withRateLimitRetry(
|
|
95
|
+
await withRateLimitRetry(() => storage.getClient().joinRoom(vaultId));
|
|
96
96
|
}
|
|
97
97
|
catch (err) {
|
|
98
|
-
throw new
|
|
98
|
+
throw new StorageError(`join failed: ${err.message}`);
|
|
99
99
|
}
|
|
100
|
-
return {
|
|
100
|
+
return { vaultId, joined: true };
|
|
101
101
|
}
|
|
102
102
|
function roomDisplayName(storage, roomId, fallbackName) {
|
|
103
103
|
const client = storage.getClient();
|
|
@@ -140,106 +140,123 @@ export async function listPendingInvites(storage) {
|
|
|
140
140
|
}
|
|
141
141
|
return invites;
|
|
142
142
|
}
|
|
143
|
-
/** Decline a
|
|
144
|
-
export async function declineInvite(storage,
|
|
143
|
+
/** Decline a vault invite (same as leaving before join). */
|
|
144
|
+
export async function declineInvite(storage, vaultId) {
|
|
145
145
|
const client = storage.getClient();
|
|
146
146
|
try {
|
|
147
|
-
await withRateLimitRetry(
|
|
148
|
-
await client.leave(
|
|
147
|
+
await withRateLimitRetry(async () => {
|
|
148
|
+
await client.leave(vaultId);
|
|
149
149
|
// The server only allows forgetting a room once this account's
|
|
150
150
|
// membership is `leave`; forgetting removes the room from the local
|
|
151
151
|
// store immediately, so a declined invite stops showing up in
|
|
152
|
-
//
|
|
152
|
+
// listVaults/listPendingInvites without waiting for the leave event
|
|
153
153
|
// to round-trip through the background sync loop.
|
|
154
|
-
await client.forget(
|
|
154
|
+
await client.forget(vaultId);
|
|
155
155
|
});
|
|
156
156
|
}
|
|
157
157
|
catch (err) {
|
|
158
|
-
throw new
|
|
158
|
+
throw new StorageError(`decline failed: ${err.message}`);
|
|
159
159
|
}
|
|
160
|
-
return {
|
|
160
|
+
return { vaultId, declined: true };
|
|
161
161
|
}
|
|
162
162
|
/**
|
|
163
|
-
* Invites `userId` to the
|
|
163
|
+
* Invites `userId` to the vault at `role` and applies the role's
|
|
164
164
|
* permissions. Doubles as "change an existing participant's role" (call
|
|
165
165
|
* again with a different role): inviting someone who's already a member is
|
|
166
166
|
* a 403 from the server, not a real failure — that specific error is
|
|
167
167
|
* swallowed and the role change still applies. Any other invite failure
|
|
168
168
|
* (e.g. unknown user) propagates.
|
|
169
169
|
*/
|
|
170
|
-
export async function
|
|
170
|
+
export async function shareVault(storage, vaultId, userId, role) {
|
|
171
171
|
if (role !== "viewer" && role !== "editor") {
|
|
172
|
-
throw new
|
|
172
|
+
throw new StorageError(`invalid --role "${role}" (must be viewer or editor)`);
|
|
173
173
|
}
|
|
174
|
-
const tree = await resolveTree(storage,
|
|
174
|
+
const tree = await resolveTree(storage, vaultId);
|
|
175
175
|
try {
|
|
176
|
-
await withRateLimitRetry(
|
|
176
|
+
await withRateLimitRetry(() => tree.invite(userId));
|
|
177
177
|
}
|
|
178
178
|
catch (err) {
|
|
179
179
|
if (!/already in the room/i.test(err.message)) {
|
|
180
180
|
throw err;
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
|
-
await withRateLimitRetry(
|
|
184
|
-
return {
|
|
183
|
+
await withRateLimitRetry(() => tree.setPermissions(userId, role));
|
|
184
|
+
return { vaultId, userId, role };
|
|
185
185
|
}
|
|
186
|
-
export async function
|
|
187
|
-
await resolveTree(storage,
|
|
186
|
+
export async function unshareVault(storage, vaultId, userId) {
|
|
187
|
+
await resolveTree(storage, vaultId);
|
|
188
188
|
try {
|
|
189
|
-
await withRateLimitRetry(
|
|
189
|
+
await withRateLimitRetry(() => storage.getClient().kick(vaultId, userId, "unshared"));
|
|
190
190
|
}
|
|
191
191
|
catch (err) {
|
|
192
|
-
throw new
|
|
192
|
+
throw new StorageError(`unshare failed: ${err.message}`);
|
|
193
193
|
}
|
|
194
|
-
return {
|
|
194
|
+
return { vaultId, userId, removed: true };
|
|
195
195
|
}
|
|
196
|
-
export async function listMembers(storage,
|
|
197
|
-
const tree = await resolveTree(storage,
|
|
198
|
-
return withRateLimitRetry(
|
|
196
|
+
export async function listMembers(storage, vaultId) {
|
|
197
|
+
const tree = await resolveTree(storage, vaultId);
|
|
198
|
+
return withRateLimitRetry(() => storage.listMembers(tree));
|
|
199
199
|
}
|
|
200
|
-
export async function listFiles(storage,
|
|
201
|
-
const tree = await resolveTree(storage,
|
|
202
|
-
await storage.refreshRoomState(
|
|
200
|
+
export async function listFiles(storage, treeId) {
|
|
201
|
+
const tree = await resolveTree(storage, treeId);
|
|
202
|
+
await storage.refreshRoomState(treeId);
|
|
203
203
|
return tree.listFiles().map((f) => ({ id: f.id, name: f.getName() }));
|
|
204
204
|
}
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
/** Lists the direct child folders of a vault or folder. */
|
|
206
|
+
export async function listSubfolders(storage, parentId) {
|
|
207
|
+
const tree = await resolveTree(storage, parentId);
|
|
207
208
|
return tree.getDirectories().map((d) => ({ id: d.id, name: d.room.name }));
|
|
208
209
|
}
|
|
209
|
-
export async function createSubfolder(storage,
|
|
210
|
-
const tree = await resolveTree(storage,
|
|
211
|
-
const sub = await withRateLimitRetry(
|
|
210
|
+
export async function createSubfolder(storage, parentId, name) {
|
|
211
|
+
const tree = await resolveTree(storage, parentId);
|
|
212
|
+
const sub = await withRateLimitRetry(() => storage.createSubtree(tree, name));
|
|
212
213
|
return { id: sub.id, name };
|
|
213
214
|
}
|
|
215
|
+
async function renameTree(storage, treeId, name) {
|
|
216
|
+
const tree = await resolveTree(storage, treeId);
|
|
217
|
+
await withRateLimitRetry(() => tree.setName(name));
|
|
218
|
+
return { id: treeId, name };
|
|
219
|
+
}
|
|
220
|
+
/** Renames a top-level Vault. */
|
|
221
|
+
export async function renameVault(storage, vaultId, name) {
|
|
222
|
+
return renameTree(storage, vaultId, name);
|
|
223
|
+
}
|
|
224
|
+
/** Renames a nested folder. */
|
|
214
225
|
export async function renameFolder(storage, folderId, name) {
|
|
215
|
-
|
|
216
|
-
await withRateLimitRetry("renameFolder", () => tree.setName(name));
|
|
217
|
-
return { id: folderId, name };
|
|
226
|
+
return renameTree(storage, folderId, name);
|
|
218
227
|
}
|
|
219
|
-
|
|
220
|
-
const tree = await resolveTree(storage,
|
|
228
|
+
async function deleteTree(storage, treeId) {
|
|
229
|
+
const tree = await resolveTree(storage, treeId);
|
|
221
230
|
// `tree.delete()` kicks members and leaves this room AND every
|
|
222
231
|
// subdirectory room, but never forgets any of them, so they all linger in
|
|
223
|
-
// the local store and
|
|
232
|
+
// the local store and tree listings keep returning the deleted tree until
|
|
224
233
|
// the leave events round-trip through the background sync loop (30-60s
|
|
225
234
|
// under full-suite load). Forgetting removes each room from the local
|
|
226
235
|
// store immediately; the server only accepts a forget once membership is
|
|
227
236
|
// `leave`, which `tree.delete()` has just completed for the whole tree.
|
|
228
237
|
const subdirectoryIds = tree.getDirectories().map((d) => d.id);
|
|
229
238
|
const client = storage.getClient();
|
|
230
|
-
await withRateLimitRetry(
|
|
239
|
+
await withRateLimitRetry(async () => {
|
|
231
240
|
await tree.delete();
|
|
232
|
-
await client.forget(
|
|
241
|
+
await client.forget(treeId);
|
|
233
242
|
for (const subId of subdirectoryIds) {
|
|
234
243
|
await client.forget(subId);
|
|
235
244
|
}
|
|
236
245
|
});
|
|
237
|
-
return { id:
|
|
246
|
+
return { id: treeId, deleted: true };
|
|
247
|
+
}
|
|
248
|
+
/** Deletes a top-level Vault. */
|
|
249
|
+
export async function deleteVault(storage, vaultId) {
|
|
250
|
+
return deleteTree(storage, vaultId);
|
|
251
|
+
}
|
|
252
|
+
/** Deletes a nested folder. */
|
|
253
|
+
export async function deleteFolder(storage, folderId) {
|
|
254
|
+
return deleteTree(storage, folderId);
|
|
238
255
|
}
|
|
239
|
-
export async function renameFile(storage,
|
|
240
|
-
const tree = await resolveTree(storage,
|
|
256
|
+
export async function renameFile(storage, treeId, fileId, name) {
|
|
257
|
+
const tree = await resolveTree(storage, treeId);
|
|
241
258
|
const branch = await resolveFile(tree, fileId);
|
|
242
|
-
await withRateLimitRetry(
|
|
259
|
+
await withRateLimitRetry(() => branch.setName(name));
|
|
243
260
|
// `setName` resolves when the homeserver accepts the state event, but a
|
|
244
261
|
// fresh CLI/UI process can still read the previous local room state for a
|
|
245
262
|
// short time. Do not report success until this client has observed the new
|
|
@@ -250,26 +267,26 @@ export async function renameFile(storage, folderId, fileId, name) {
|
|
|
250
267
|
}, { timeoutMs: 15000 });
|
|
251
268
|
return { id: fileId, name };
|
|
252
269
|
}
|
|
253
|
-
export async function deleteFile(storage,
|
|
254
|
-
const tree = await resolveTree(storage,
|
|
270
|
+
export async function deleteFile(storage, treeId, fileId) {
|
|
271
|
+
const tree = await resolveTree(storage, treeId);
|
|
255
272
|
const branch = await resolveFile(tree, fileId);
|
|
256
|
-
await withRateLimitRetry(
|
|
273
|
+
await withRateLimitRetry(() => branch.delete());
|
|
257
274
|
return { id: fileId, deleted: true };
|
|
258
275
|
}
|
|
259
|
-
export async function uploadFile(storage,
|
|
260
|
-
const tree = await resolveTree(storage,
|
|
261
|
-
const fileId = await withRateLimitRetry(
|
|
276
|
+
export async function uploadFile(storage, treeId, name, bytes, mimetype) {
|
|
277
|
+
const tree = await resolveTree(storage, treeId);
|
|
278
|
+
const fileId = await withRateLimitRetry(() => storage.uploadFile(tree, name, toArrayBuffer(bytes), mimetype));
|
|
262
279
|
return { id: fileId, name, mimetype };
|
|
263
280
|
}
|
|
264
|
-
export async function downloadFile(storage,
|
|
265
|
-
const tree = await resolveTree(storage,
|
|
281
|
+
export async function downloadFile(storage, treeId, fileId) {
|
|
282
|
+
const tree = await resolveTree(storage, treeId);
|
|
266
283
|
const branch = await resolveFile(tree, fileId);
|
|
267
284
|
let result;
|
|
268
285
|
try {
|
|
269
286
|
result = await storage.downloadFile(branch);
|
|
270
287
|
}
|
|
271
288
|
catch (err) {
|
|
272
|
-
throw new
|
|
289
|
+
throw new StorageError(`download failed: ${err.message}`);
|
|
273
290
|
}
|
|
274
291
|
return {
|
|
275
292
|
bytes: new Uint8Array(result.data),
|
|
@@ -288,8 +305,8 @@ function tsToIso(ts) {
|
|
|
288
305
|
return null;
|
|
289
306
|
return new Date(ts).toISOString();
|
|
290
307
|
}
|
|
291
|
-
export async function getFileDetails(storage,
|
|
292
|
-
const tree = await resolveTree(storage,
|
|
308
|
+
export async function getFileDetails(storage, treeId, fileId) {
|
|
309
|
+
const tree = await resolveTree(storage, treeId);
|
|
293
310
|
const branch = await resolveFile(tree, fileId);
|
|
294
311
|
const name = branch.getName();
|
|
295
312
|
let mimetype = null;
|
|
@@ -328,10 +345,10 @@ export async function getFileDetails(storage, folderId, fileId) {
|
|
|
328
345
|
}
|
|
329
346
|
return { name, mimetype, size, createdAt, updatedAt };
|
|
330
347
|
}
|
|
331
|
-
|
|
332
|
-
const tree = await resolveTree(storage,
|
|
348
|
+
async function getTreeDetails(storage, treeId) {
|
|
349
|
+
const tree = await resolveTree(storage, treeId);
|
|
333
350
|
const client = storage.getClient();
|
|
334
|
-
const room = client.getRoom(
|
|
351
|
+
const room = client.getRoom(treeId);
|
|
335
352
|
let createdAt = null;
|
|
336
353
|
let memberCount = null;
|
|
337
354
|
if (room) {
|
|
@@ -345,9 +362,17 @@ export async function getFolderDetails(storage, folderId) {
|
|
|
345
362
|
}
|
|
346
363
|
}
|
|
347
364
|
return {
|
|
348
|
-
name: tree.room.name || roomDisplayName(storage,
|
|
349
|
-
id:
|
|
365
|
+
name: tree.room.name || roomDisplayName(storage, treeId),
|
|
366
|
+
id: treeId,
|
|
350
367
|
createdAt,
|
|
351
368
|
memberCount,
|
|
352
369
|
};
|
|
353
370
|
}
|
|
371
|
+
/** Returns details for a top-level Vault. */
|
|
372
|
+
export async function getVaultDetails(storage, vaultId) {
|
|
373
|
+
return getTreeDetails(storage, vaultId);
|
|
374
|
+
}
|
|
375
|
+
/** Returns details for a nested folder. */
|
|
376
|
+
export async function getFolderDetails(storage, folderId) {
|
|
377
|
+
return getTreeDetails(storage, folderId);
|
|
378
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* directly. Keeping it here means the CLI and the UI can never drift apart
|
|
8
|
-
* on what an operation returns.
|
|
9
|
-
*/
|
|
1
|
+
/** The typed result contract for every `core/operations.ts` function. */
|
|
2
|
+
export interface VaultInfo {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
/** A nested directory node inside a vault. */
|
|
10
7
|
export interface FolderInfo {
|
|
11
8
|
id: string;
|
|
12
9
|
name: string;
|
|
@@ -22,17 +19,17 @@ export interface Member {
|
|
|
22
19
|
membership: string;
|
|
23
20
|
}
|
|
24
21
|
export interface ShareResult {
|
|
25
|
-
|
|
22
|
+
vaultId: string;
|
|
26
23
|
userId: string;
|
|
27
24
|
role: string;
|
|
28
25
|
}
|
|
29
26
|
export interface UnshareResult {
|
|
30
|
-
|
|
27
|
+
vaultId: string;
|
|
31
28
|
userId: string;
|
|
32
29
|
removed: boolean;
|
|
33
30
|
}
|
|
34
31
|
export interface JoinResult {
|
|
35
|
-
|
|
32
|
+
vaultId: string;
|
|
36
33
|
joined: boolean;
|
|
37
34
|
}
|
|
38
35
|
export interface DeleteResult {
|
|
@@ -63,6 +60,13 @@ export interface FileDetails {
|
|
|
63
60
|
createdAt: string | null;
|
|
64
61
|
updatedAt: string | null;
|
|
65
62
|
}
|
|
63
|
+
export interface VaultDetails {
|
|
64
|
+
name: string;
|
|
65
|
+
id: string;
|
|
66
|
+
createdAt: string | null;
|
|
67
|
+
memberCount: number | null;
|
|
68
|
+
}
|
|
69
|
+
/** Details for a nested directory node inside a vault. */
|
|
66
70
|
export interface FolderDetails {
|
|
67
71
|
name: string;
|
|
68
72
|
id: string;
|
package/dist/core/types.js
CHANGED
|
@@ -1,10 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The shared typed result contract for every `core/operations.ts` function.
|
|
3
|
-
* This is deliberately the ONE definition of these shapes: the CLI's
|
|
4
|
-
* `--json` schema is these types (or a trivial projection of them, e.g.
|
|
5
|
-
* `FolderInfo.id` -> the CLI's `folderId` key for backwards-compatible
|
|
6
|
-
* command output), and the future React UI's data model is these types
|
|
7
|
-
* directly. Keeping it here means the CLI and the UI can never drift apart
|
|
8
|
-
* on what an operation returns.
|
|
9
|
-
*/
|
|
1
|
+
/** The typed result contract for every `core/operations.ts` function. */
|
|
10
2
|
export {};
|