@telecrypt-io/storage 0.1.3 → 0.2.1
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 +9 -15
- package/dist/TeleCryptIOStorage.d.ts +7 -0
- package/dist/TeleCryptIOStorage.js +14 -1
- package/dist/core/operations.js +9 -0
- package/package.json +7 -10
- package/dist/cli/cryptoSnapshot.d.ts +0 -37
- package/dist/cli/cryptoSnapshot.js +0 -187
- package/dist/cli/errors.d.ts +0 -1
- package/dist/cli/errors.js +0 -3
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -443
- package/dist/cli/oidc.d.ts +0 -19
- package/dist/cli/oidc.js +0 -97
- package/dist/cli/oidcWindowPolyfill.d.ts +0 -1
- package/dist/cli/oidcWindowPolyfill.js +0 -76
- package/dist/cli/output.d.ts +0 -16
- package/dist/cli/output.js +0 -24
- package/dist/cli/poll.d.ts +0 -1
- package/dist/cli/poll.js +0 -3
- package/dist/cli/profile.d.ts +0 -33
- package/dist/cli/profile.js +0 -46
- package/dist/cli/storage.d.ts +0 -36
- package/dist/cli/storage.js +0 -126
package/README.md
CHANGED
|
@@ -8,7 +8,9 @@ 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
|
-
**Status:**
|
|
11
|
+
**Status:** library source only. `@telecrypt-io/storage@0.2.0` is the current public release,
|
|
12
|
+
published from this repository's protected `v0.2.0` tag with NPM Trusted Publishing/OIDC
|
|
13
|
+
provenance. The older `0.1.3` release is the immutable legacy combined library-and-CLI package.
|
|
12
14
|
|
|
13
15
|
## Install
|
|
14
16
|
|
|
@@ -16,7 +18,7 @@ Recovery Key restores your files on a new device — even if you lose the origin
|
|
|
16
18
|
npm install @telecrypt-io/storage
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
This gives you
|
|
21
|
+
This gives you the `TeleCryptIOStorage` library and its browser-safe `core` API.
|
|
20
22
|
|
|
21
23
|
## Quick start
|
|
22
24
|
|
|
@@ -33,17 +35,10 @@ const folder = await core.createFolder(storage, "Photos");
|
|
|
33
35
|
await core.uploadFile(storage, folder.id, "cat.jpg", bytes, "image/jpeg");
|
|
34
36
|
```
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
telecrypt-io storage folder create Photos
|
|
41
|
-
telecrypt-io storage file upload <folderId> ./cat.jpg
|
|
42
|
-
telecrypt-io storage folder share <folderId> @bob:your.server --role editor
|
|
43
|
-
telecrypt-io storage recovery setup # prints your Recovery Key — save it
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
**Web UI:** a React app lives in [`ui/`](./ui) (`cd ui && npm install && npm run dev`).
|
|
38
|
+
The command-line client is sourced by
|
|
39
|
+
[`TeleCrypt-io/storage-cli`](https://github.com/TeleCrypt-io/storage-cli). The static web
|
|
40
|
+
application is sourced by
|
|
41
|
+
[`TeleCrypt-io/storage.telecrypt.io`](https://github.com/TeleCrypt-io/storage.telecrypt.io).
|
|
47
42
|
|
|
48
43
|
## How it works
|
|
49
44
|
|
|
@@ -74,8 +69,7 @@ npm run synapse:down
|
|
|
74
69
|
|
|
75
70
|
Tests run against a real local Synapse in podman, never against a production server.
|
|
76
71
|
|
|
77
|
-
See [
|
|
78
|
-
instructions.
|
|
72
|
+
See [RELEASING.md](./RELEASING.md) for the guarded npm release procedure.
|
|
79
73
|
|
|
80
74
|
## Licence
|
|
81
75
|
|
|
@@ -117,6 +117,13 @@ export declare class TeleCryptIOStorage {
|
|
|
117
117
|
constructor(client: MatrixClient);
|
|
118
118
|
/** The underlying matrix-js-sdk client (e.g. to stop it, or for advanced/interop use). */
|
|
119
119
|
getClient(): MatrixClient;
|
|
120
|
+
/**
|
|
121
|
+
* Refreshes a room's current state from the homeserver. The background sync
|
|
122
|
+
* loop is intentionally asynchronous, so a short-lived CLI process can
|
|
123
|
+
* otherwise list a stale state snapshot immediately after another process
|
|
124
|
+
* changed a file name or branch.
|
|
125
|
+
*/
|
|
126
|
+
refreshRoomState(roomId: string): Promise<void>;
|
|
120
127
|
/**
|
|
121
128
|
* Recommended entry point: builds the MatrixClient with a persistent crypto
|
|
122
129
|
* store and the secret-storage callback wiring `keys.*` needs, starts the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ClientEvent, createClient, SyncState } from "matrix-js-sdk";
|
|
1
|
+
import { ClientEvent, createClient, MatrixEvent, SyncState } 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";
|
|
@@ -12,6 +12,19 @@ export class TeleCryptIOStorage {
|
|
|
12
12
|
getClient() {
|
|
13
13
|
return this.client;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Refreshes a room's current state from the homeserver. The background sync
|
|
17
|
+
* loop is intentionally asynchronous, so a short-lived CLI process can
|
|
18
|
+
* otherwise list a stale state snapshot immediately after another process
|
|
19
|
+
* changed a file name or branch.
|
|
20
|
+
*/
|
|
21
|
+
async refreshRoomState(roomId) {
|
|
22
|
+
const room = this.client.getRoom(roomId);
|
|
23
|
+
if (!room)
|
|
24
|
+
throw new Error(`Unknown room: ${roomId}`);
|
|
25
|
+
const stateEvents = await this.client.roomState(roomId);
|
|
26
|
+
room.currentState.setStateEvents(stateEvents.map((event) => new MatrixEvent(event)));
|
|
27
|
+
}
|
|
15
28
|
/**
|
|
16
29
|
* Recommended entry point: builds the MatrixClient with a persistent crypto
|
|
17
30
|
* store and the secret-storage callback wiring `keys.*` needs, starts the
|
package/dist/core/operations.js
CHANGED
|
@@ -148,6 +148,7 @@ export async function listMembers(storage, folderId) {
|
|
|
148
148
|
}
|
|
149
149
|
export async function listFiles(storage, folderId) {
|
|
150
150
|
const tree = await resolveTree(storage, folderId);
|
|
151
|
+
await storage.refreshRoomState(folderId);
|
|
151
152
|
return tree.listFiles().map((f) => ({ id: f.id, name: f.getName() }));
|
|
152
153
|
}
|
|
153
154
|
export async function listSubfolders(storage, folderId) {
|
|
@@ -173,6 +174,14 @@ export async function renameFile(storage, folderId, fileId, name) {
|
|
|
173
174
|
const tree = await resolveTree(storage, folderId);
|
|
174
175
|
const branch = await resolveFile(tree, fileId);
|
|
175
176
|
await branch.setName(name);
|
|
177
|
+
// `setName` resolves when the homeserver accepts the state event, but a
|
|
178
|
+
// fresh CLI/UI process can still read the previous local room state for a
|
|
179
|
+
// short time. Do not report success until this client has observed the new
|
|
180
|
+
// name through its normal sync loop.
|
|
181
|
+
await waitForCondition(() => {
|
|
182
|
+
const current = tree.getFile(fileId);
|
|
183
|
+
return current?.getName() === name ? current : null;
|
|
184
|
+
}, { timeoutMs: 15000 });
|
|
176
185
|
return { id: fileId, name };
|
|
177
186
|
}
|
|
178
187
|
export async function deleteFile(storage, folderId, fileId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telecrypt-io/storage",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"license": "BUSL-1.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,9 +15,6 @@
|
|
|
15
15
|
"import": "./dist/core/index.js"
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
|
-
"bin": {
|
|
19
|
-
"telecrypt-io": "dist/cli/index.js"
|
|
20
|
-
},
|
|
21
18
|
"files": [
|
|
22
19
|
"dist",
|
|
23
20
|
"README.md",
|
|
@@ -37,10 +34,9 @@
|
|
|
37
34
|
"synapse:down": "./throwaway_synapse/down.sh"
|
|
38
35
|
},
|
|
39
36
|
"dependencies": {
|
|
40
|
-
"commander": "15.0.0",
|
|
41
|
-
"fake-indexeddb": "6.2.5",
|
|
42
37
|
"matrix-encrypt-attachment": "1.0.3",
|
|
43
|
-
"matrix-js-sdk": "41.9.0"
|
|
38
|
+
"matrix-js-sdk": "41.9.0",
|
|
39
|
+
"oidc-client-ts": "3.5.0"
|
|
44
40
|
},
|
|
45
41
|
"devDependencies": {
|
|
46
42
|
"@playwright/test": "1.61.1",
|
|
@@ -48,16 +44,17 @@
|
|
|
48
44
|
"@typescript-eslint/eslint-plugin": "8.64.0",
|
|
49
45
|
"@typescript-eslint/parser": "8.64.0",
|
|
50
46
|
"eslint": "10.8.0",
|
|
47
|
+
"fake-indexeddb": "6.2.5",
|
|
51
48
|
"tsx": "4.23.1",
|
|
52
49
|
"typescript": "5.9.3",
|
|
53
50
|
"vitest": "4.1.10"
|
|
54
51
|
},
|
|
55
52
|
"repository": {
|
|
56
53
|
"type": "git",
|
|
57
|
-
"url": "git+https://github.com/TeleCrypt-io/
|
|
54
|
+
"url": "git+https://github.com/TeleCrypt-io/storage-sdk.git"
|
|
58
55
|
},
|
|
59
|
-
"homepage": "https://github.com/TeleCrypt-io/
|
|
56
|
+
"homepage": "https://github.com/TeleCrypt-io/storage-sdk#readme",
|
|
60
57
|
"bugs": {
|
|
61
|
-
"url": "https://github.com/TeleCrypt-io/
|
|
58
|
+
"url": "https://github.com/TeleCrypt-io/storage-sdk/issues"
|
|
62
59
|
}
|
|
63
60
|
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
interface IndexSpec {
|
|
2
|
-
name: string;
|
|
3
|
-
keyPath: string | string[];
|
|
4
|
-
unique: boolean;
|
|
5
|
-
multiEntry: boolean;
|
|
6
|
-
}
|
|
7
|
-
interface StoreSpec {
|
|
8
|
-
name: string;
|
|
9
|
-
keyPath: string | string[] | null;
|
|
10
|
-
autoIncrement: boolean;
|
|
11
|
-
indexes: IndexSpec[];
|
|
12
|
-
}
|
|
13
|
-
interface StoreRecord {
|
|
14
|
-
key?: unknown;
|
|
15
|
-
value: unknown;
|
|
16
|
-
}
|
|
17
|
-
interface DbSnapshot {
|
|
18
|
-
name: string;
|
|
19
|
-
version: number;
|
|
20
|
-
stores: StoreSpec[];
|
|
21
|
-
records: Record<string, StoreRecord[]>;
|
|
22
|
-
}
|
|
23
|
-
export interface CryptoSnapshot {
|
|
24
|
-
dbs: DbSnapshot[];
|
|
25
|
-
}
|
|
26
|
-
/** Dumps every IndexedDB database currently visible to this process. */
|
|
27
|
-
export declare function exportIndexedDB(): Promise<CryptoSnapshot>;
|
|
28
|
-
/** Recreates every database/store/index/record from a snapshot into the
|
|
29
|
-
* current (assumed empty) IndexedDB factory. */
|
|
30
|
-
export declare function importIndexedDB(snapshot: CryptoSnapshot): Promise<void>;
|
|
31
|
-
export declare function loadSnapshotFromDisk(path: string): CryptoSnapshot | null;
|
|
32
|
-
export declare function saveSnapshotToDisk(path: string, snapshot: CryptoSnapshot): void;
|
|
33
|
-
/** Loads the on-disk snapshot (if any) into the current process's fake-indexeddb. */
|
|
34
|
-
export declare function restoreCryptoStore(path: string): Promise<void>;
|
|
35
|
-
/** Dumps the current process's fake-indexeddb to disk for the next process. */
|
|
36
|
-
export declare function persistCryptoStore(path: string): Promise<void>;
|
|
37
|
-
export {};
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cross-process crypto persistence for the process-per-command CLI.
|
|
3
|
-
*
|
|
4
|
-
* A CLI runs each command as a separate OS process. `TeleCryptIOStorage.create()`
|
|
5
|
-
* initialises rust-crypto with `useIndexedDB: true`, but in Node the only
|
|
6
|
-
* IndexedDB available is `fake-indexeddb`, which is a pure in-memory
|
|
7
|
-
* implementation — it evaporates when the process exits. Left alone, every
|
|
8
|
-
* CLI invocation would start with an empty crypto store and be unable to
|
|
9
|
-
* decrypt anything a previous invocation wrote (including its own device's
|
|
10
|
-
* megolm sessions), which also breaks cross-user sharing: userB's upload
|
|
11
|
-
* process sends userA an olm-encrypted room key addressed to userA's device
|
|
12
|
-
* identity; if that identity is regenerated every run, the key is
|
|
13
|
-
* undecryptable and userA can never read userB's file no matter what backup
|
|
14
|
-
* scheme is layered on top.
|
|
15
|
-
*
|
|
16
|
-
* Fix: snapshot fake-indexeddb's databases to disk after each command and
|
|
17
|
-
* reload them before the next one runs, keyed to the profile directory. This
|
|
18
|
-
* is Option 1 from the spec (disk-persistent crypto store) implemented via
|
|
19
|
-
* generic export/import over the *public* IndexedDB API (databases(), cursors,
|
|
20
|
-
* transactions) rather than poking fake-indexeddb's internals — so it isn't
|
|
21
|
-
* coupled to fake-indexeddb's private representation and would keep working
|
|
22
|
-
* against any spec-compliant IndexedDB implementation. Runtime crypto
|
|
23
|
-
* behaviour is otherwise unchanged from what the library's own tests already
|
|
24
|
-
* exercise (keys.test.ts, tree.test.ts 4.6 both run rust-crypto on
|
|
25
|
-
* fake-indexeddb).
|
|
26
|
-
*
|
|
27
|
-
* Binary values (megolm session keys etc.) don't survive JSON, so the
|
|
28
|
-
* snapshot is serialised with Node's structured-clone-capable `node:v8`
|
|
29
|
-
* (de)serialize rather than JSON.
|
|
30
|
-
*/
|
|
31
|
-
import * as fs from "node:fs";
|
|
32
|
-
import * as v8 from "node:v8";
|
|
33
|
-
function getIndexedDB() {
|
|
34
|
-
const idb = globalThis.indexedDB;
|
|
35
|
-
if (!idb) {
|
|
36
|
-
throw new Error("cryptoSnapshot: globalThis.indexedDB is not set — import 'fake-indexeddb/auto' before calling exportIndexedDB/importIndexedDB");
|
|
37
|
-
}
|
|
38
|
-
return idb;
|
|
39
|
-
}
|
|
40
|
-
function promisifyRequest(req) {
|
|
41
|
-
return new Promise((resolve, reject) => {
|
|
42
|
-
req.onsuccess = () => resolve(req.result);
|
|
43
|
-
req.onerror = () => reject(req.error);
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
function promisifyTxDone(tx) {
|
|
47
|
-
return new Promise((resolve, reject) => {
|
|
48
|
-
tx.oncomplete = () => resolve();
|
|
49
|
-
tx.onerror = () => reject(tx.error);
|
|
50
|
-
tx.onabort = () => reject(tx.error ?? new Error("IndexedDB transaction aborted"));
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
function readAllRecords(store) {
|
|
54
|
-
return new Promise((resolve, reject) => {
|
|
55
|
-
const out = [];
|
|
56
|
-
const outOfLine = store.keyPath === null;
|
|
57
|
-
const req = store.openCursor();
|
|
58
|
-
req.onerror = () => reject(req.error);
|
|
59
|
-
req.onsuccess = () => {
|
|
60
|
-
const cursor = req.result;
|
|
61
|
-
if (cursor) {
|
|
62
|
-
out.push(outOfLine
|
|
63
|
-
? { key: cursor.primaryKey, value: cursor.value }
|
|
64
|
-
: { value: cursor.value });
|
|
65
|
-
cursor.continue();
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
resolve(out);
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
/** Dumps every IndexedDB database currently visible to this process. */
|
|
74
|
-
export async function exportIndexedDB() {
|
|
75
|
-
const idb = getIndexedDB();
|
|
76
|
-
const infos = await idb.databases();
|
|
77
|
-
const dbs = [];
|
|
78
|
-
for (const info of infos) {
|
|
79
|
-
if (!info.name)
|
|
80
|
-
continue;
|
|
81
|
-
const db = await promisifyRequest(idb.open(info.name, info.version));
|
|
82
|
-
try {
|
|
83
|
-
const storeNames = Array.from(db.objectStoreNames);
|
|
84
|
-
const stores = [];
|
|
85
|
-
const records = {};
|
|
86
|
-
if (storeNames.length > 0) {
|
|
87
|
-
const tx = db.transaction(storeNames, "readonly");
|
|
88
|
-
for (const storeName of storeNames) {
|
|
89
|
-
const store = tx.objectStore(storeName);
|
|
90
|
-
const indexes = Array.from(store.indexNames).map((iname) => {
|
|
91
|
-
const idx = store.index(iname);
|
|
92
|
-
return {
|
|
93
|
-
name: idx.name,
|
|
94
|
-
keyPath: idx.keyPath,
|
|
95
|
-
unique: idx.unique,
|
|
96
|
-
multiEntry: idx.multiEntry,
|
|
97
|
-
};
|
|
98
|
-
});
|
|
99
|
-
stores.push({
|
|
100
|
-
name: storeName,
|
|
101
|
-
keyPath: store.keyPath,
|
|
102
|
-
autoIncrement: store.autoIncrement,
|
|
103
|
-
indexes,
|
|
104
|
-
});
|
|
105
|
-
records[storeName] = await readAllRecords(store);
|
|
106
|
-
}
|
|
107
|
-
await promisifyTxDone(tx);
|
|
108
|
-
}
|
|
109
|
-
dbs.push({ name: info.name, version: db.version, stores, records });
|
|
110
|
-
}
|
|
111
|
-
finally {
|
|
112
|
-
db.close();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return { dbs };
|
|
116
|
-
}
|
|
117
|
-
/** Recreates every database/store/index/record from a snapshot into the
|
|
118
|
-
* current (assumed empty) IndexedDB factory. */
|
|
119
|
-
export async function importIndexedDB(snapshot) {
|
|
120
|
-
const idb = getIndexedDB();
|
|
121
|
-
for (const dbSnap of snapshot.dbs) {
|
|
122
|
-
const db = await new Promise((resolve, reject) => {
|
|
123
|
-
const req = idb.open(dbSnap.name, Math.max(dbSnap.version, 1));
|
|
124
|
-
req.onupgradeneeded = () => {
|
|
125
|
-
const database = req.result;
|
|
126
|
-
for (const store of dbSnap.stores) {
|
|
127
|
-
const os = database.createObjectStore(store.name, {
|
|
128
|
-
keyPath: store.keyPath ?? undefined,
|
|
129
|
-
autoIncrement: store.autoIncrement,
|
|
130
|
-
});
|
|
131
|
-
for (const idx of store.indexes) {
|
|
132
|
-
os.createIndex(idx.name, idx.keyPath, {
|
|
133
|
-
unique: idx.unique,
|
|
134
|
-
multiEntry: idx.multiEntry,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
req.onsuccess = () => resolve(req.result);
|
|
140
|
-
req.onerror = () => reject(req.error);
|
|
141
|
-
});
|
|
142
|
-
try {
|
|
143
|
-
const storeNames = dbSnap.stores.map((s) => s.name);
|
|
144
|
-
if (storeNames.length > 0) {
|
|
145
|
-
const tx = db.transaction(storeNames, "readwrite");
|
|
146
|
-
for (const store of dbSnap.stores) {
|
|
147
|
-
const os = tx.objectStore(store.name);
|
|
148
|
-
for (const rec of dbSnap.records[store.name] ?? []) {
|
|
149
|
-
if (store.keyPath === null) {
|
|
150
|
-
os.put(rec.value, rec.key);
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
os.put(rec.value);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
await promisifyTxDone(tx);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
finally {
|
|
161
|
-
db.close();
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
export function loadSnapshotFromDisk(path) {
|
|
166
|
-
if (!fs.existsSync(path))
|
|
167
|
-
return null;
|
|
168
|
-
const buf = fs.readFileSync(path);
|
|
169
|
-
if (buf.length === 0)
|
|
170
|
-
return null;
|
|
171
|
-
return v8.deserialize(buf);
|
|
172
|
-
}
|
|
173
|
-
export function saveSnapshotToDisk(path, snapshot) {
|
|
174
|
-
fs.writeFileSync(path, v8.serialize(snapshot), { mode: 0o600 });
|
|
175
|
-
}
|
|
176
|
-
/** Loads the on-disk snapshot (if any) into the current process's fake-indexeddb. */
|
|
177
|
-
export async function restoreCryptoStore(path) {
|
|
178
|
-
const snapshot = loadSnapshotFromDisk(path);
|
|
179
|
-
if (snapshot) {
|
|
180
|
-
await importIndexedDB(snapshot);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
/** Dumps the current process's fake-indexeddb to disk for the next process. */
|
|
184
|
-
export async function persistCryptoStore(path) {
|
|
185
|
-
const snapshot = await exportIndexedDB();
|
|
186
|
-
saveSnapshotToDisk(path, snapshot);
|
|
187
|
-
}
|
package/dist/cli/errors.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { CliError } from "../core/errors.js";
|
package/dist/cli/errors.js
DELETED
package/dist/cli/index.d.ts
DELETED