@telecrypt-io/storage 0.1.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/LICENSE +109 -0
- package/README.md +53 -0
- package/dist/TeleCryptIOStorage.d.ts +185 -0
- package/dist/TeleCryptIOStorage.js +323 -0
- package/dist/cli/cryptoSnapshot.d.ts +37 -0
- package/dist/cli/cryptoSnapshot.js +187 -0
- package/dist/cli/errors.d.ts +1 -0
- package/dist/cli/errors.js +3 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +415 -0
- package/dist/cli/output.d.ts +16 -0
- package/dist/cli/output.js +24 -0
- package/dist/cli/poll.d.ts +1 -0
- package/dist/cli/poll.js +3 -0
- package/dist/cli/profile.d.ts +20 -0
- package/dist/cli/profile.js +46 -0
- package/dist/cli/storage.d.ts +36 -0
- package/dist/cli/storage.js +99 -0
- package/dist/core/errors.d.ts +8 -0
- package/dist/core/errors.js +11 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +5 -0
- package/dist/core/operations.d.ts +35 -0
- package/dist/core/operations.js +120 -0
- package/dist/core/poll.d.ts +14 -0
- package/dist/core/poll.js +24 -0
- package/dist/core/types.d.ts +50 -0
- package/dist/core/types.js +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
function isJsonMode(command) {
|
|
2
|
+
return Boolean(command.optsWithGlobals().json);
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Runs a command action: executes `fn`, prints its result (JSON or human
|
|
6
|
+
* text) to stdout on success, or a clean `{ "error": "..." }` (JSON mode) /
|
|
7
|
+
* `Error: ...` (text mode) to stderr on failure — never a raw stack trace.
|
|
8
|
+
* Sets process.exitCode accordingly (0 on success, 1 on failure) rather than
|
|
9
|
+
* calling process.exit() directly, so callers can do any final cleanup
|
|
10
|
+
* first.
|
|
11
|
+
*/
|
|
12
|
+
export async function runAction(command, fn) {
|
|
13
|
+
const json = isJsonMode(command);
|
|
14
|
+
try {
|
|
15
|
+
const result = await fn();
|
|
16
|
+
process.stdout.write((json ? JSON.stringify(result.json) : result.text) + "\n");
|
|
17
|
+
process.exitCode = 0;
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21
|
+
process.stderr.write((json ? JSON.stringify({ error: message }) : `Error: ${message}`) + "\n");
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { waitForCondition } from "../core/poll.js";
|
package/dist/cli/poll.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface Session {
|
|
2
|
+
homeserver: string;
|
|
3
|
+
userId: string;
|
|
4
|
+
deviceId: string;
|
|
5
|
+
accessToken: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the profile directory: everything this CLI persists (session,
|
|
9
|
+
* crypto store) lives here. Overridable via TELECRYPT_IO_STORAGE_HOME so
|
|
10
|
+
* tests can give each simulated user/device its own isolated profile.
|
|
11
|
+
* Defaults to ~/.telecrypt-io/storage for normal interactive use.
|
|
12
|
+
*/
|
|
13
|
+
export declare function profileDir(): string;
|
|
14
|
+
export declare function sessionPath(dir?: string): string;
|
|
15
|
+
export declare function cryptoSnapshotPath(dir?: string): string;
|
|
16
|
+
export declare function ensureProfileDir(dir?: string): void;
|
|
17
|
+
export declare function readSession(dir?: string): Session | null;
|
|
18
|
+
export declare function writeSession(session: Session, dir?: string): void;
|
|
19
|
+
/** Clears all local state for this profile (session + crypto store). */
|
|
20
|
+
export declare function clearProfile(dir?: string): void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the profile directory: everything this CLI persists (session,
|
|
6
|
+
* crypto store) lives here. Overridable via TELECRYPT_IO_STORAGE_HOME so
|
|
7
|
+
* tests can give each simulated user/device its own isolated profile.
|
|
8
|
+
* Defaults to ~/.telecrypt-io/storage for normal interactive use.
|
|
9
|
+
*/
|
|
10
|
+
export function profileDir() {
|
|
11
|
+
const home = process.env.TELECRYPT_IO_STORAGE_HOME;
|
|
12
|
+
if (home && home.trim() !== "")
|
|
13
|
+
return home;
|
|
14
|
+
return path.join(os.homedir(), ".telecrypt-io", "storage");
|
|
15
|
+
}
|
|
16
|
+
export function sessionPath(dir = profileDir()) {
|
|
17
|
+
return path.join(dir, "session.json");
|
|
18
|
+
}
|
|
19
|
+
export function cryptoSnapshotPath(dir = profileDir()) {
|
|
20
|
+
return path.join(dir, "crypto.snapshot");
|
|
21
|
+
}
|
|
22
|
+
export function ensureProfileDir(dir = profileDir()) {
|
|
23
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
24
|
+
}
|
|
25
|
+
export function readSession(dir = profileDir()) {
|
|
26
|
+
const p = sessionPath(dir);
|
|
27
|
+
if (!fs.existsSync(p))
|
|
28
|
+
return null;
|
|
29
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
export function writeSession(session, dir = profileDir()) {
|
|
33
|
+
ensureProfileDir(dir);
|
|
34
|
+
fs.writeFileSync(sessionPath(dir), JSON.stringify(session, null, 2), {
|
|
35
|
+
mode: 0o600,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** Clears all local state for this profile (session + crypto store). */
|
|
39
|
+
export function clearProfile(dir = profileDir()) {
|
|
40
|
+
const sp = sessionPath(dir);
|
|
41
|
+
const cp = cryptoSnapshotPath(dir);
|
|
42
|
+
if (fs.existsSync(sp))
|
|
43
|
+
fs.rmSync(sp);
|
|
44
|
+
if (fs.existsSync(cp))
|
|
45
|
+
fs.rmSync(cp);
|
|
46
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import "fake-indexeddb/auto";
|
|
2
|
+
import { TeleCryptIOStorage } from "../TeleCryptIOStorage.js";
|
|
3
|
+
import { Session } from "./profile.js";
|
|
4
|
+
export interface OpenedStorage {
|
|
5
|
+
storage: TeleCryptIOStorage;
|
|
6
|
+
session: Session;
|
|
7
|
+
/** Persists the crypto store back to disk and stops the client. Call this
|
|
8
|
+
* in a `finally` block around every command that opens storage, so the
|
|
9
|
+
* next CLI invocation sees whatever this one learned (new megolm sessions,
|
|
10
|
+
* device keys, etc.) — this is what makes decryption survive across
|
|
11
|
+
* separate processes. */
|
|
12
|
+
close: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Opens a TeleCryptIOStorage bound to the current profile's session, having first
|
|
16
|
+
* restored the crypto store snapshot (if any) from a previous CLI
|
|
17
|
+
* invocation. Throws CliError("not logged in") if there is no session.
|
|
18
|
+
*/
|
|
19
|
+
export declare function openStorage(dir?: string): Promise<OpenedStorage>;
|
|
20
|
+
/**
|
|
21
|
+
* If server-side key backup is active for this account, waits (best-effort,
|
|
22
|
+
* bounded) for the SDK's backup engine to report zero sessions remaining to
|
|
23
|
+
* upload. This matters specifically because a CLI command is a *short-lived
|
|
24
|
+
* process*: the backup engine deliberately fire-and-forgets its upload loop
|
|
25
|
+
* with a randomised 0-10s startup jitter (to avoid a multi-device thundering
|
|
26
|
+
* herd), so a command that creates a new megolm session (recovery setup,
|
|
27
|
+
* or a file upload once recovery is already set up) and then exits
|
|
28
|
+
* immediately can easily outrun that loop, leaving the new key silently
|
|
29
|
+
* absent from the backup — recoverable-looking but not actually recoverable.
|
|
30
|
+
* Never throws: this is a best-effort settle grace period, not a
|
|
31
|
+
* correctness gate on the command's primary result.
|
|
32
|
+
*/
|
|
33
|
+
export declare function waitForBackupSettled(storage: TeleCryptIOStorage, timeoutMs?: number): Promise<void>;
|
|
34
|
+
/** Used by login/register: builds storage for a brand-new session and
|
|
35
|
+
* establishes the initial (empty) crypto store snapshot on disk. */
|
|
36
|
+
export declare function initStorageForNewSession(session: Session, dir?: string): Promise<OpenedStorage>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import "fake-indexeddb/auto";
|
|
2
|
+
import { CryptoEvent } from "matrix-js-sdk/lib/crypto-api/index.js";
|
|
3
|
+
import { TeleCryptIOStorage } from "../TeleCryptIOStorage.js";
|
|
4
|
+
import { cryptoSnapshotPath, ensureProfileDir, profileDir, readSession } from "./profile.js";
|
|
5
|
+
import { persistCryptoStore, restoreCryptoStore } from "./cryptoSnapshot.js";
|
|
6
|
+
import { CliError } from "./errors.js";
|
|
7
|
+
/**
|
|
8
|
+
* Opens a TeleCryptIOStorage bound to the current profile's session, having first
|
|
9
|
+
* restored the crypto store snapshot (if any) from a previous CLI
|
|
10
|
+
* invocation. Throws CliError("not logged in") if there is no session.
|
|
11
|
+
*/
|
|
12
|
+
export async function openStorage(dir = profileDir()) {
|
|
13
|
+
const session = readSession(dir);
|
|
14
|
+
if (!session) {
|
|
15
|
+
throw new CliError("not logged in — run `telecrypt-io storage login` first");
|
|
16
|
+
}
|
|
17
|
+
const snapshotPath = cryptoSnapshotPath(dir);
|
|
18
|
+
await restoreCryptoStore(snapshotPath);
|
|
19
|
+
const storage = await TeleCryptIOStorage.create({
|
|
20
|
+
baseUrl: session.homeserver,
|
|
21
|
+
userId: session.userId,
|
|
22
|
+
accessToken: session.accessToken,
|
|
23
|
+
deviceId: session.deviceId,
|
|
24
|
+
});
|
|
25
|
+
let closed = false;
|
|
26
|
+
const close = async () => {
|
|
27
|
+
if (closed)
|
|
28
|
+
return;
|
|
29
|
+
closed = true;
|
|
30
|
+
try {
|
|
31
|
+
await persistCryptoStore(snapshotPath);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
storage.getClient().stopClient();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
return { storage, session, close };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* If server-side key backup is active for this account, waits (best-effort,
|
|
41
|
+
* bounded) for the SDK's backup engine to report zero sessions remaining to
|
|
42
|
+
* upload. This matters specifically because a CLI command is a *short-lived
|
|
43
|
+
* process*: the backup engine deliberately fire-and-forgets its upload loop
|
|
44
|
+
* with a randomised 0-10s startup jitter (to avoid a multi-device thundering
|
|
45
|
+
* herd), so a command that creates a new megolm session (recovery setup,
|
|
46
|
+
* or a file upload once recovery is already set up) and then exits
|
|
47
|
+
* immediately can easily outrun that loop, leaving the new key silently
|
|
48
|
+
* absent from the backup — recoverable-looking but not actually recoverable.
|
|
49
|
+
* Never throws: this is a best-effort settle grace period, not a
|
|
50
|
+
* correctness gate on the command's primary result.
|
|
51
|
+
*/
|
|
52
|
+
export async function waitForBackupSettled(storage, timeoutMs = 20000) {
|
|
53
|
+
const active = await storage.keys.isRecoverySetup();
|
|
54
|
+
if (!active)
|
|
55
|
+
return;
|
|
56
|
+
const client = storage.getClient();
|
|
57
|
+
await new Promise((resolve) => {
|
|
58
|
+
let settled = false;
|
|
59
|
+
const finish = () => {
|
|
60
|
+
if (settled)
|
|
61
|
+
return;
|
|
62
|
+
settled = true;
|
|
63
|
+
client.removeListener(CryptoEvent.KeyBackupSessionsRemaining, onRemaining);
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
resolve();
|
|
66
|
+
};
|
|
67
|
+
const onRemaining = (remaining) => {
|
|
68
|
+
if (remaining === 0)
|
|
69
|
+
finish();
|
|
70
|
+
};
|
|
71
|
+
client.on(CryptoEvent.KeyBackupSessionsRemaining, onRemaining);
|
|
72
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** Used by login/register: builds storage for a brand-new session and
|
|
76
|
+
* establishes the initial (empty) crypto store snapshot on disk. */
|
|
77
|
+
export async function initStorageForNewSession(session, dir = profileDir()) {
|
|
78
|
+
ensureProfileDir(dir);
|
|
79
|
+
const snapshotPath = cryptoSnapshotPath(dir);
|
|
80
|
+
const storage = await TeleCryptIOStorage.create({
|
|
81
|
+
baseUrl: session.homeserver,
|
|
82
|
+
userId: session.userId,
|
|
83
|
+
accessToken: session.accessToken,
|
|
84
|
+
deviceId: session.deviceId,
|
|
85
|
+
});
|
|
86
|
+
let closed = false;
|
|
87
|
+
const close = async () => {
|
|
88
|
+
if (closed)
|
|
89
|
+
return;
|
|
90
|
+
closed = true;
|
|
91
|
+
try {
|
|
92
|
+
await persistCryptoStore(snapshotPath);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
storage.getClient().stopClient();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
return { storage, session, close };
|
|
99
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** A user-facing operation error: a clean, prefix-free message meant to be
|
|
2
|
+
* shown directly to a user (CLI stderr today, a future UI's error toast
|
|
3
|
+
* tomorrow) — never a raw stack trace. Thrown for expected failure
|
|
4
|
+
* conditions (bad login, wrong recovery key, missing file/folder, not
|
|
5
|
+
* logged in, ...). Platform-agnostic: no Node/CLI dependencies. */
|
|
6
|
+
export declare class CliError extends Error {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** A user-facing operation error: a clean, prefix-free message meant to be
|
|
2
|
+
* shown directly to a user (CLI stderr today, a future UI's error toast
|
|
3
|
+
* tomorrow) — never a raw stack trace. Thrown for expected failure
|
|
4
|
+
* conditions (bad login, wrong recovery key, missing file/folder, not
|
|
5
|
+
* logged in, ...). Platform-agnostic: no Node/CLI dependencies. */
|
|
6
|
+
export class CliError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "CliError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-agnostic operations: one function per user-facing action, each
|
|
3
|
+
* taking an already-created `TeleCryptIOStorage` plus plain inputs and returning
|
|
4
|
+
* one of the typed results in `./types.ts`. No I/O beyond the Matrix client
|
|
5
|
+
* itself, no stdout, no `process`, no file paths — bytes in/out are always
|
|
6
|
+
* `Uint8Array`. This is what the CLI's command actions call today and what
|
|
7
|
+
* a future React UI calls directly, so both run the exact same tested logic
|
|
8
|
+
* and share the exact same result shapes.
|
|
9
|
+
*
|
|
10
|
+
* `core/` never creates the `TeleCryptIOStorage`/`MatrixClient` itself — store
|
|
11
|
+
* config (persistent crypto store, session credentials, etc.) is
|
|
12
|
+
* platform-specific and stays with the caller (see `src/cli/storage.ts`).
|
|
13
|
+
*/
|
|
14
|
+
import { TeleCryptIOStorage } from "../TeleCryptIOStorage.js";
|
|
15
|
+
import type { DownloadedFile, FileInfo, FolderInfo, JoinResult, Member, RecoveryRestore, RecoverySetup, ShareResult, UnshareResult } from "./types.js";
|
|
16
|
+
export declare function createFolder(storage: TeleCryptIOStorage, name: string): Promise<FolderInfo>;
|
|
17
|
+
/** Top-level folders only — excludes subdirectories of an existing tree. */
|
|
18
|
+
export declare function listFolders(storage: TeleCryptIOStorage): Promise<FolderInfo[]>;
|
|
19
|
+
export declare function joinFolder(storage: TeleCryptIOStorage, folderId: string): Promise<JoinResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Invites `userId` to the folder at `role` and applies the role's
|
|
22
|
+
* permissions. Doubles as "change an existing participant's role" (call
|
|
23
|
+
* again with a different role): inviting someone who's already a member is
|
|
24
|
+
* a 403 from the server, not a real failure — that specific error is
|
|
25
|
+
* swallowed and the role change still applies. Any other invite failure
|
|
26
|
+
* (e.g. unknown user) propagates.
|
|
27
|
+
*/
|
|
28
|
+
export declare function shareFolder(storage: TeleCryptIOStorage, folderId: string, userId: string, role: string): Promise<ShareResult>;
|
|
29
|
+
export declare function unshareFolder(storage: TeleCryptIOStorage, folderId: string, userId: string): Promise<UnshareResult>;
|
|
30
|
+
export declare function listMembers(storage: TeleCryptIOStorage, folderId: string): Promise<Member[]>;
|
|
31
|
+
export declare function listFiles(storage: TeleCryptIOStorage, folderId: string): Promise<FileInfo[]>;
|
|
32
|
+
export declare function uploadFile(storage: TeleCryptIOStorage, folderId: string, name: string, bytes: Uint8Array, mimetype: string): Promise<FileInfo>;
|
|
33
|
+
export declare function downloadFile(storage: TeleCryptIOStorage, folderId: string, fileId: string): Promise<DownloadedFile>;
|
|
34
|
+
export declare function setupRecovery(storage: TeleCryptIOStorage): Promise<RecoverySetup>;
|
|
35
|
+
export declare function restoreRecovery(storage: TeleCryptIOStorage, recoveryKey: string): Promise<RecoveryRestore>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { CliError } from "./errors.js";
|
|
2
|
+
import { waitForCondition } from "./poll.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolves a folder by ID, polling briefly: a room this same account just
|
|
5
|
+
* created (or was just invited to, by another process/session) can be
|
|
6
|
+
* momentarily absent from a from-scratch `/sync` before showing up moments
|
|
7
|
+
* later — real async settling, not an instant "not found". Throws a clean
|
|
8
|
+
* error if the folder still isn't visible once the poll times out.
|
|
9
|
+
*/
|
|
10
|
+
async function resolveTree(storage, folderId) {
|
|
11
|
+
try {
|
|
12
|
+
return await waitForCondition(() => storage.getTree(folderId), {
|
|
13
|
+
timeoutMs: 15000,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new CliError(`folder not found: ${folderId}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** As `resolveTree`, but for a specific file within an already-resolved
|
|
21
|
+
* folder — covers the same settling window for a file another
|
|
22
|
+
* process/session just uploaded. */
|
|
23
|
+
async function resolveFile(tree, fileId) {
|
|
24
|
+
try {
|
|
25
|
+
return await waitForCondition(() => tree.getFile(fileId), { timeoutMs: 15000 });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new CliError(`file not found: ${fileId}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function toArrayBuffer(bytes) {
|
|
32
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
33
|
+
}
|
|
34
|
+
export async function createFolder(storage, name) {
|
|
35
|
+
const tree = await storage.createTree(name);
|
|
36
|
+
return { id: tree.id, name };
|
|
37
|
+
}
|
|
38
|
+
/** Top-level folders only — excludes subdirectories of an existing tree. */
|
|
39
|
+
export async function listFolders(storage) {
|
|
40
|
+
const trees = await storage.listTrees();
|
|
41
|
+
return trees.filter((t) => t.isTopLevel).map((t) => ({ id: t.id, name: t.room.name }));
|
|
42
|
+
}
|
|
43
|
+
export async function joinFolder(storage, folderId) {
|
|
44
|
+
try {
|
|
45
|
+
await storage.getClient().joinRoom(folderId);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
throw new CliError(`join failed: ${err.message}`);
|
|
49
|
+
}
|
|
50
|
+
return { folderId, joined: true };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Invites `userId` to the folder at `role` and applies the role's
|
|
54
|
+
* permissions. Doubles as "change an existing participant's role" (call
|
|
55
|
+
* again with a different role): inviting someone who's already a member is
|
|
56
|
+
* a 403 from the server, not a real failure — that specific error is
|
|
57
|
+
* swallowed and the role change still applies. Any other invite failure
|
|
58
|
+
* (e.g. unknown user) propagates.
|
|
59
|
+
*/
|
|
60
|
+
export async function shareFolder(storage, folderId, userId, role) {
|
|
61
|
+
if (role !== "viewer" && role !== "editor") {
|
|
62
|
+
throw new CliError(`invalid --role "${role}" (must be viewer or editor)`);
|
|
63
|
+
}
|
|
64
|
+
const tree = await resolveTree(storage, folderId);
|
|
65
|
+
try {
|
|
66
|
+
await tree.invite(userId);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
if (!/already in the room/i.test(err.message)) {
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
await tree.setPermissions(userId, role);
|
|
74
|
+
return { folderId, userId, role };
|
|
75
|
+
}
|
|
76
|
+
export async function unshareFolder(storage, folderId, userId) {
|
|
77
|
+
await resolveTree(storage, folderId);
|
|
78
|
+
try {
|
|
79
|
+
await storage.getClient().kick(folderId, userId, "unshared");
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
throw new CliError(`unshare failed: ${err.message}`);
|
|
83
|
+
}
|
|
84
|
+
return { folderId, userId, removed: true };
|
|
85
|
+
}
|
|
86
|
+
export async function listMembers(storage, folderId) {
|
|
87
|
+
const tree = await resolveTree(storage, folderId);
|
|
88
|
+
return storage.listMembers(tree);
|
|
89
|
+
}
|
|
90
|
+
export async function listFiles(storage, folderId) {
|
|
91
|
+
const tree = await resolveTree(storage, folderId);
|
|
92
|
+
return tree.listFiles().map((f) => ({ id: f.id, name: f.getName() }));
|
|
93
|
+
}
|
|
94
|
+
export async function uploadFile(storage, folderId, name, bytes, mimetype) {
|
|
95
|
+
const tree = await resolveTree(storage, folderId);
|
|
96
|
+
const fileId = await storage.uploadFile(tree, name, toArrayBuffer(bytes), mimetype);
|
|
97
|
+
return { id: fileId, name, mimetype };
|
|
98
|
+
}
|
|
99
|
+
export async function downloadFile(storage, folderId, fileId) {
|
|
100
|
+
const tree = await resolveTree(storage, folderId);
|
|
101
|
+
const branch = await resolveFile(tree, fileId);
|
|
102
|
+
let result;
|
|
103
|
+
try {
|
|
104
|
+
result = await storage.downloadFile(branch);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
throw new CliError(`download failed: ${err.message}`);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
bytes: new Uint8Array(result.data),
|
|
111
|
+
mimetype: result.mimetype,
|
|
112
|
+
name: branch.getName(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export async function setupRecovery(storage) {
|
|
116
|
+
return storage.keys.setupRecovery();
|
|
117
|
+
}
|
|
118
|
+
export async function restoreRecovery(storage, recoveryKey) {
|
|
119
|
+
return storage.keys.restoreFromRecoveryKey(recoveryKey);
|
|
120
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A freshly-synced client doesn't always see a room/event the *instant* the
|
|
3
|
+
* server has committed it — e.g. a room this same session just created can
|
|
4
|
+
* briefly be absent from the very next from-scratch `/sync` a later process
|
|
5
|
+
* performs, before showing up moments later. That's real async settling
|
|
6
|
+
* (Matrix eventual-consistency-on-fresh-sync), not a bug to paper over with
|
|
7
|
+
* a fixed delay: poll the actual condition, bounded by a timeout, leveraging
|
|
8
|
+
* the client's own live background sync loop (already running in this
|
|
9
|
+
* process since TeleCryptIOStorage.create() started it).
|
|
10
|
+
*/
|
|
11
|
+
export declare function waitForCondition<T>(check: () => T | null | undefined, opts?: {
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
intervalMs?: number;
|
|
14
|
+
}): Promise<T>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A freshly-synced client doesn't always see a room/event the *instant* the
|
|
3
|
+
* server has committed it — e.g. a room this same session just created can
|
|
4
|
+
* briefly be absent from the very next from-scratch `/sync` a later process
|
|
5
|
+
* performs, before showing up moments later. That's real async settling
|
|
6
|
+
* (Matrix eventual-consistency-on-fresh-sync), not a bug to paper over with
|
|
7
|
+
* a fixed delay: poll the actual condition, bounded by a timeout, leveraging
|
|
8
|
+
* the client's own live background sync loop (already running in this
|
|
9
|
+
* process since TeleCryptIOStorage.create() started it).
|
|
10
|
+
*/
|
|
11
|
+
export async function waitForCondition(check, opts) {
|
|
12
|
+
const timeoutMs = opts?.timeoutMs ?? 15000;
|
|
13
|
+
const intervalMs = opts?.intervalMs ?? 300;
|
|
14
|
+
const deadline = Date.now() + timeoutMs;
|
|
15
|
+
for (;;) {
|
|
16
|
+
const result = check();
|
|
17
|
+
if (result !== null && result !== undefined)
|
|
18
|
+
return result;
|
|
19
|
+
if (Date.now() >= deadline) {
|
|
20
|
+
throw new Error(`timed out after ${timeoutMs}ms waiting for condition`);
|
|
21
|
+
}
|
|
22
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
*/
|
|
10
|
+
export interface FolderInfo {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
export interface FileInfo {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
mimetype?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface Member {
|
|
20
|
+
userId: string;
|
|
21
|
+
role: string;
|
|
22
|
+
membership: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ShareResult {
|
|
25
|
+
folderId: string;
|
|
26
|
+
userId: string;
|
|
27
|
+
role: string;
|
|
28
|
+
}
|
|
29
|
+
export interface UnshareResult {
|
|
30
|
+
folderId: string;
|
|
31
|
+
userId: string;
|
|
32
|
+
removed: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface JoinResult {
|
|
35
|
+
folderId: string;
|
|
36
|
+
joined: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** Bytes in/out are always `Uint8Array` — never a file path. */
|
|
39
|
+
export interface DownloadedFile {
|
|
40
|
+
bytes: Uint8Array;
|
|
41
|
+
mimetype: string;
|
|
42
|
+
name: string;
|
|
43
|
+
}
|
|
44
|
+
export interface RecoverySetup {
|
|
45
|
+
recoveryKey: string;
|
|
46
|
+
}
|
|
47
|
+
export interface RecoveryRestore {
|
|
48
|
+
imported: number;
|
|
49
|
+
total: number;
|
|
50
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
*/
|
|
10
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TeleCryptIOStorage } from "./TeleCryptIOStorage.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TeleCryptIOStorage } from "./TeleCryptIOStorage.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@telecrypt-io/storage",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "BUSL-1.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"telecrypt-io": "dist/cli/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"provenance": true
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"lint": "eslint src test --ext .ts",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest",
|
|
31
|
+
"synapse:up": "./throwaway_synapse/up.sh",
|
|
32
|
+
"synapse:fresh": "./throwaway_synapse/up.sh --fresh",
|
|
33
|
+
"synapse:down": "./throwaway_synapse/down.sh"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"commander": "^15.0.0",
|
|
37
|
+
"fake-indexeddb": "^6.2.5",
|
|
38
|
+
"matrix-encrypt-attachment": "1.0.3",
|
|
39
|
+
"matrix-js-sdk": "41.9.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.0.0",
|
|
43
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
44
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
45
|
+
"eslint": "^9.0.0",
|
|
46
|
+
"tsx": "^4.23.1",
|
|
47
|
+
"typescript": "^5.6.0",
|
|
48
|
+
"vitest": "^2.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|