@telecrypt-io/storage 0.2.13 → 0.3.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 +6 -7
- package/dist/TeleCryptIOStorage.js +1 -2
- package/dist/core/errors.d.ts +2 -3
- package/dist/core/errors.js +3 -4
- 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 +3 -4
- package/dist/core/operations.js +22 -22
- package/dist/core/types.d.ts +1 -9
- package/dist/core/types.js +1 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,14 +8,13 @@ Files are encrypted on the client before upload. The server stores only opaque c
|
|
|
8
8
|
never holds the decryption keys. Shared folders 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.3.0
|
|
19
18
|
```
|
|
20
19
|
|
|
21
20
|
This gives you the `TeleCryptIOStorage` library and its browser-safe `core` API.
|
|
@@ -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
|
|
|
@@ -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
|
-
* tomorrow) — never a raw stack trace. Thrown for expected failure
|
|
2
|
+
* shown directly to a user — never a raw stack trace. Thrown for expected failure
|
|
4
3
|
* conditions (bad login, wrong recovery key, missing file/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
|
-
* tomorrow) — never a raw stack trace. Thrown for expected failure
|
|
2
|
+
* shown directly to a user — never a raw stack trace. Thrown for expected failure
|
|
4
3
|
* conditions (bad login, wrong recovery key, missing file/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,13 +3,12 @@
|
|
|
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
14
|
import type { DownloadedFile, FileDetails, FileInfo, FolderDetails, FolderInfo, DeleteResult, JoinResult, Member, RecoveryRestore, RecoverySetup, RenameResult, ShareResult, UnshareResult } from "./types.js";
|
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();
|
|
@@ -56,7 +56,7 @@ async function resolveTree(storage, folderId) {
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
catch {
|
|
59
|
-
throw new
|
|
59
|
+
throw new StorageError(`folder not found: ${folderId}`);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
/** As `resolveTree`, but for a specific file within an already-resolved
|
|
@@ -67,14 +67,14 @@ async function resolveFile(tree, fileId) {
|
|
|
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
76
|
export async function createFolder(storage, name) {
|
|
77
|
-
const tree = await withRateLimitRetry(
|
|
77
|
+
const tree = await withRateLimitRetry(() => storage.createTree(name));
|
|
78
78
|
return { id: tree.id, name };
|
|
79
79
|
}
|
|
80
80
|
/** Top-level folders only — excludes subdirectories of an existing tree. */
|
|
@@ -92,10 +92,10 @@ export function getMyFolderRole(storage, folderId) {
|
|
|
92
92
|
}
|
|
93
93
|
export async function joinFolder(storage, folderId) {
|
|
94
94
|
try {
|
|
95
|
-
await withRateLimitRetry(
|
|
95
|
+
await withRateLimitRetry(() => storage.getClient().joinRoom(folderId));
|
|
96
96
|
}
|
|
97
97
|
catch (err) {
|
|
98
|
-
throw new
|
|
98
|
+
throw new StorageError(`join failed: ${err.message}`);
|
|
99
99
|
}
|
|
100
100
|
return { folderId, joined: true };
|
|
101
101
|
}
|
|
@@ -144,7 +144,7 @@ export async function listPendingInvites(storage) {
|
|
|
144
144
|
export async function declineInvite(storage, folderId) {
|
|
145
145
|
const client = storage.getClient();
|
|
146
146
|
try {
|
|
147
|
-
await withRateLimitRetry(
|
|
147
|
+
await withRateLimitRetry(async () => {
|
|
148
148
|
await client.leave(folderId);
|
|
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
|
|
@@ -155,7 +155,7 @@ export async function declineInvite(storage, folderId) {
|
|
|
155
155
|
});
|
|
156
156
|
}
|
|
157
157
|
catch (err) {
|
|
158
|
-
throw new
|
|
158
|
+
throw new StorageError(`decline failed: ${err.message}`);
|
|
159
159
|
}
|
|
160
160
|
return { folderId, declined: true };
|
|
161
161
|
}
|
|
@@ -169,33 +169,33 @@ export async function declineInvite(storage, folderId) {
|
|
|
169
169
|
*/
|
|
170
170
|
export async function shareFolder(storage, folderId, 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
174
|
const tree = await resolveTree(storage, folderId);
|
|
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(
|
|
183
|
+
await withRateLimitRetry(() => tree.setPermissions(userId, role));
|
|
184
184
|
return { folderId, userId, role };
|
|
185
185
|
}
|
|
186
186
|
export async function unshareFolder(storage, folderId, userId) {
|
|
187
187
|
await resolveTree(storage, folderId);
|
|
188
188
|
try {
|
|
189
|
-
await withRateLimitRetry(
|
|
189
|
+
await withRateLimitRetry(() => storage.getClient().kick(folderId, userId, "unshared"));
|
|
190
190
|
}
|
|
191
191
|
catch (err) {
|
|
192
|
-
throw new
|
|
192
|
+
throw new StorageError(`unshare failed: ${err.message}`);
|
|
193
193
|
}
|
|
194
194
|
return { folderId, userId, removed: true };
|
|
195
195
|
}
|
|
196
196
|
export async function listMembers(storage, folderId) {
|
|
197
197
|
const tree = await resolveTree(storage, folderId);
|
|
198
|
-
return withRateLimitRetry(
|
|
198
|
+
return withRateLimitRetry(() => storage.listMembers(tree));
|
|
199
199
|
}
|
|
200
200
|
export async function listFiles(storage, folderId) {
|
|
201
201
|
const tree = await resolveTree(storage, folderId);
|
|
@@ -208,12 +208,12 @@ export async function listSubfolders(storage, folderId) {
|
|
|
208
208
|
}
|
|
209
209
|
export async function createSubfolder(storage, folderId, name) {
|
|
210
210
|
const tree = await resolveTree(storage, folderId);
|
|
211
|
-
const sub = await withRateLimitRetry(
|
|
211
|
+
const sub = await withRateLimitRetry(() => storage.createSubtree(tree, name));
|
|
212
212
|
return { id: sub.id, name };
|
|
213
213
|
}
|
|
214
214
|
export async function renameFolder(storage, folderId, name) {
|
|
215
215
|
const tree = await resolveTree(storage, folderId);
|
|
216
|
-
await withRateLimitRetry(
|
|
216
|
+
await withRateLimitRetry(() => tree.setName(name));
|
|
217
217
|
return { id: folderId, name };
|
|
218
218
|
}
|
|
219
219
|
export async function deleteFolder(storage, folderId) {
|
|
@@ -227,7 +227,7 @@ export async function deleteFolder(storage, folderId) {
|
|
|
227
227
|
// `leave`, which `tree.delete()` has just completed for the whole tree.
|
|
228
228
|
const subdirectoryIds = tree.getDirectories().map((d) => d.id);
|
|
229
229
|
const client = storage.getClient();
|
|
230
|
-
await withRateLimitRetry(
|
|
230
|
+
await withRateLimitRetry(async () => {
|
|
231
231
|
await tree.delete();
|
|
232
232
|
await client.forget(folderId);
|
|
233
233
|
for (const subId of subdirectoryIds) {
|
|
@@ -239,7 +239,7 @@ export async function deleteFolder(storage, folderId) {
|
|
|
239
239
|
export async function renameFile(storage, folderId, fileId, name) {
|
|
240
240
|
const tree = await resolveTree(storage, folderId);
|
|
241
241
|
const branch = await resolveFile(tree, fileId);
|
|
242
|
-
await withRateLimitRetry(
|
|
242
|
+
await withRateLimitRetry(() => branch.setName(name));
|
|
243
243
|
// `setName` resolves when the homeserver accepts the state event, but a
|
|
244
244
|
// fresh CLI/UI process can still read the previous local room state for a
|
|
245
245
|
// short time. Do not report success until this client has observed the new
|
|
@@ -253,12 +253,12 @@ export async function renameFile(storage, folderId, fileId, name) {
|
|
|
253
253
|
export async function deleteFile(storage, folderId, fileId) {
|
|
254
254
|
const tree = await resolveTree(storage, folderId);
|
|
255
255
|
const branch = await resolveFile(tree, fileId);
|
|
256
|
-
await withRateLimitRetry(
|
|
256
|
+
await withRateLimitRetry(() => branch.delete());
|
|
257
257
|
return { id: fileId, deleted: true };
|
|
258
258
|
}
|
|
259
259
|
export async function uploadFile(storage, folderId, name, bytes, mimetype) {
|
|
260
260
|
const tree = await resolveTree(storage, folderId);
|
|
261
|
-
const fileId = await withRateLimitRetry(
|
|
261
|
+
const fileId = await withRateLimitRetry(() => storage.uploadFile(tree, name, toArrayBuffer(bytes), mimetype));
|
|
262
262
|
return { id: fileId, name, mimetype };
|
|
263
263
|
}
|
|
264
264
|
export async function downloadFile(storage, folderId, fileId) {
|
|
@@ -269,7 +269,7 @@ export async function downloadFile(storage, folderId, fileId) {
|
|
|
269
269
|
result = await storage.downloadFile(branch);
|
|
270
270
|
}
|
|
271
271
|
catch (err) {
|
|
272
|
-
throw new
|
|
272
|
+
throw new StorageError(`download failed: ${err.message}`);
|
|
273
273
|
}
|
|
274
274
|
return {
|
|
275
275
|
bytes: new Uint8Array(result.data),
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
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 interface FolderInfo {
|
|
11
3
|
id: string;
|
|
12
4
|
name: 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 {};
|