@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 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:** published and usable — library, CLI, and a React web UI.
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 both the `TeleCryptIOStorage` library and the `telecrypt-io` CLI.
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
- **CLI:**
37
-
38
- ```bash
39
- telecrypt-io storage login --homeserver https://your.server --user alice --password ...
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 [CLI.md](./CLI.md) for command reference and [RELEASING.md](./RELEASING.md) for release
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
@@ -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",
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/secure-storage.git"
54
+ "url": "git+https://github.com/TeleCrypt-io/storage-sdk.git"
58
55
  },
59
- "homepage": "https://github.com/TeleCrypt-io/secure-storage#readme",
56
+ "homepage": "https://github.com/TeleCrypt-io/storage-sdk#readme",
60
57
  "bugs": {
61
- "url": "https://github.com/TeleCrypt-io/secure-storage/issues"
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
- }
@@ -1 +0,0 @@
1
- export { CliError } from "../core/errors.js";
@@ -1,3 +0,0 @@
1
- // Re-homed under src/core/errors.ts (platform-agnostic — no Node/CLI
2
- // dependencies), re-exported here so existing CLI imports keep working.
3
- export { CliError } from "../core/errors.js";
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import "fake-indexeddb/auto";