@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/dist/cli/index.js DELETED
@@ -1,443 +0,0 @@
1
- #!/usr/bin/env node
2
- import "fake-indexeddb/auto";
3
- import * as fs from "node:fs";
4
- import * as path from "node:path";
5
- import { Command } from "commander";
6
- import { createClient } from "matrix-js-sdk";
7
- import { clearProfile, readSession, writeSession } from "./profile.js";
8
- import { initStorageForNewSession, openStorage, waitForBackupSettled } from "./storage.js";
9
- import { CliError } from "./errors.js";
10
- import { runAction } from "./output.js";
11
- import * as core from "../core/operations.js";
12
- import { runDeviceCodeLogin } from "./oidc.js";
13
- // matrix-js-sdk (loglevel) and the rust-crypto WASM tracing layer write
14
- // verbose logs straight to console.log/debug/info/trace (stdout by default)
15
- // AND console.warn/error (stderr by default) — e.g. push-rule setup notices
16
- // and background-request warnings fire on totally successful runs. Left
17
- // alone, that corrupts BOTH halves of the CLI's output contract: stdout must
18
- // be exactly one line (human text or --json payload), and stderr under
19
- // --json must be exactly one `{"error": "..."}` line for a test (or script)
20
- // to parse. Silence all of them here, before TeleCryptIOStorage.create() ever
21
- // triggers rust-crypto initialisation; the CLI's own output always goes
22
- // through process.stdout.write/process.stderr.write directly (see
23
- // output.ts), never console.*, so this can't swallow anything we emit
24
- // ourselves. Set TELECRYPT_IO_STORAGE_DEBUG=1 to see the SDK's logs again
25
- // (all routed to stderr, labelled) when troubleshooting.
26
- if (!process.env.TELECRYPT_IO_STORAGE_DEBUG) {
27
- console.log = () => { };
28
- console.debug = () => { };
29
- console.info = () => { };
30
- console.trace = () => { };
31
- console.warn = () => { };
32
- console.error = () => { };
33
- }
34
- else {
35
- const toStderr = (label) => (...args) => process.stderr.write(`[${label}] ${args.map(String).join(" ")}\n`);
36
- console.log = toStderr("log");
37
- console.debug = toStderr("debug");
38
- console.info = toStderr("info");
39
- console.trace = toStderr("trace");
40
- console.warn = toStderr("warn");
41
- console.error = toStderr("error");
42
- }
43
- const EXT_MIMETYPES = {
44
- ".txt": "text/plain",
45
- ".json": "application/json",
46
- ".png": "image/png",
47
- ".jpg": "image/jpeg",
48
- ".jpeg": "image/jpeg",
49
- ".pdf": "application/pdf",
50
- ".md": "text/markdown",
51
- };
52
- function guessMimetype(filePath) {
53
- return EXT_MIMETYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
54
- }
55
- const program = new Command();
56
- program
57
- .name("telecrypt-io")
58
- .description("TeleCrypt.io CLI")
59
- .option("--json", "machine-readable JSON output")
60
- .showHelpAfterError();
61
- const storage = program
62
- .command("storage")
63
- .description("End-to-end encrypted file storage on Matrix");
64
- // ---------------------------------------------------------------------------
65
- // Session
66
- // ---------------------------------------------------------------------------
67
- storage
68
- .command("login")
69
- .description("Log in and persist the session + crypto store to the profile")
70
- .requiredOption("--homeserver <url>", "Matrix homeserver base URL")
71
- .option("--user <localpart>", "Username (localpart or full MXID) — password login")
72
- .option("--password <pw>", "Password — password login")
73
- .option("--oidc", "Log in via OIDC/MAS device-code grant instead of password")
74
- .action(async (opts, command) => {
75
- await runAction(command, async () => {
76
- let session;
77
- if (opts.oidc) {
78
- session = await runDeviceCodeLogin(opts.homeserver, {
79
- onVerification: ({ verificationUri, verificationUriComplete, userCode }) => {
80
- // Progress output — stderr only, so it never corrupts the
81
- // stdout contract (--json's single JSON line / text mode's
82
- // single line), same convention as TELECRYPT_IO_STORAGE_DEBUG
83
- // SDK logging below.
84
- process.stderr.write([
85
- "",
86
- `To finish logging in, visit: ${verificationUriComplete ?? verificationUri}`,
87
- verificationUriComplete ? "" : `and enter code: ${userCode}`,
88
- "Attempting to open your browser…",
89
- "Waiting for approval…",
90
- "",
91
- ]
92
- .filter((l) => l !== "")
93
- .join("\n") + "\n");
94
- },
95
- });
96
- }
97
- else {
98
- if (!opts.user || !opts.password) {
99
- throw new CliError("--user and --password are required for password login (or pass --oidc)");
100
- }
101
- const client = createClient({ baseUrl: opts.homeserver });
102
- let res;
103
- try {
104
- res = await client.loginWithPassword(opts.user, opts.password);
105
- }
106
- catch (err) {
107
- throw new CliError(`login failed: ${err.message}`);
108
- }
109
- session = {
110
- homeserver: opts.homeserver,
111
- userId: res.user_id,
112
- deviceId: res.device_id,
113
- accessToken: res.access_token,
114
- };
115
- }
116
- writeSession(session);
117
- // Establishes this device's crypto identity and does a first sync
118
- // (proves connectivity end-to-end), and writes the initial crypto
119
- // store snapshot so later commands have something to load.
120
- const opened = await initStorageForNewSession(session);
121
- await opened.close();
122
- return {
123
- json: { userId: session.userId, deviceId: session.deviceId, homeserver: session.homeserver },
124
- text: `Logged in as ${session.userId} (device ${session.deviceId})`,
125
- };
126
- });
127
- });
128
- storage
129
- .command("register")
130
- .description("Register a new account (dev/test convenience), then log in")
131
- .requiredOption("--homeserver <url>", "Matrix homeserver base URL")
132
- .requiredOption("--user <localpart>", "Username (localpart)")
133
- .requiredOption("--password <pw>", "Password")
134
- .action(async (opts, command) => {
135
- await runAction(command, async () => {
136
- const res = await fetch(`${opts.homeserver}/_matrix/client/v3/register`, {
137
- method: "POST",
138
- headers: { "Content-Type": "application/json" },
139
- body: JSON.stringify({
140
- username: opts.user,
141
- password: opts.password,
142
- auth: { type: "m.login.dummy" },
143
- inhibit_login: false,
144
- }),
145
- });
146
- if (!res.ok) {
147
- const body = await res.text();
148
- throw new CliError(`registration failed (${res.status}): ${body}`);
149
- }
150
- const data = (await res.json());
151
- const session = {
152
- homeserver: opts.homeserver,
153
- userId: data.user_id,
154
- deviceId: data.device_id,
155
- accessToken: data.access_token,
156
- };
157
- writeSession(session);
158
- const opened = await initStorageForNewSession(session);
159
- await opened.close();
160
- return {
161
- json: { userId: session.userId, deviceId: session.deviceId, homeserver: session.homeserver },
162
- text: `Registered and logged in as ${session.userId} (device ${session.deviceId})`,
163
- };
164
- });
165
- });
166
- storage
167
- .command("whoami")
168
- .description("Print the current session identity")
169
- .action(async (_opts, command) => {
170
- await runAction(command, async () => {
171
- const session = readSession();
172
- if (!session)
173
- throw new CliError("not logged in");
174
- return {
175
- json: { userId: session.userId, deviceId: session.deviceId, homeserver: session.homeserver },
176
- text: `${session.userId} (device ${session.deviceId}) @ ${session.homeserver}`,
177
- };
178
- });
179
- });
180
- storage
181
- .command("logout")
182
- .description("Clear the local profile (session + crypto store)")
183
- .action(async (_opts, command) => {
184
- await runAction(command, async () => {
185
- const session = readSession();
186
- if (session) {
187
- // Best-effort server-side logout; local profile is cleared either way.
188
- await fetch(`${session.homeserver}/_matrix/client/v3/logout`, {
189
- method: "POST",
190
- headers: { Authorization: `Bearer ${session.accessToken}` },
191
- }).catch(() => undefined);
192
- }
193
- clearProfile();
194
- return { json: { loggedOut: true }, text: "Logged out." };
195
- });
196
- });
197
- // ---------------------------------------------------------------------------
198
- // Recovery (Layer 2)
199
- // ---------------------------------------------------------------------------
200
- const recovery = storage.command("recovery").description("Server-side key backup / recovery");
201
- recovery
202
- .command("setup")
203
- .description("Set up recovery (cross-signing + key backup) and print the Recovery Key")
204
- .action(async (_opts, command) => {
205
- await runAction(command, async () => {
206
- const opened = await openStorage();
207
- try {
208
- const result = await core.setupRecovery(opened.storage);
209
- // Give any already-known megolm sessions a chance to actually reach
210
- // the server backup before this short-lived process exits — see
211
- // waitForBackupSettled's doc comment.
212
- await waitForBackupSettled(opened.storage);
213
- return {
214
- json: { ...result },
215
- text: [
216
- "Recovery Key (SAVE THIS — it is the only way to recover your files on a new device):",
217
- "",
218
- result.recoveryKey,
219
- ].join("\n"),
220
- };
221
- }
222
- finally {
223
- await opened.close();
224
- }
225
- });
226
- });
227
- recovery
228
- .command("restore <recoveryKey>")
229
- .description("Restore keys on this device from a Recovery Key")
230
- .action(async (recoveryKey, _opts, command) => {
231
- await runAction(command, async () => {
232
- const opened = await openStorage();
233
- try {
234
- const result = await core.restoreRecovery(opened.storage, recoveryKey);
235
- return {
236
- json: { ...result },
237
- text: `Restored ${result.imported}/${result.total} keys.`,
238
- };
239
- }
240
- finally {
241
- await opened.close();
242
- }
243
- });
244
- });
245
- // ---------------------------------------------------------------------------
246
- // Folders
247
- // ---------------------------------------------------------------------------
248
- const folder = storage.command("folder").description("Shared folder operations");
249
- folder
250
- .command("create <name>")
251
- .description("Create a new shared folder")
252
- .action(async (name, _opts, command) => {
253
- await runAction(command, async () => {
254
- const opened = await openStorage();
255
- try {
256
- const result = await core.createFolder(opened.storage, name);
257
- return {
258
- json: { folderId: result.id, name: result.name },
259
- text: `Created folder "${result.name}" (${result.id})`,
260
- };
261
- }
262
- finally {
263
- await opened.close();
264
- }
265
- });
266
- });
267
- folder
268
- .command("list")
269
- .description("List folders visible to the current user")
270
- .action(async (_opts, command) => {
271
- await runAction(command, async () => {
272
- const opened = await openStorage();
273
- try {
274
- const folders = await core.listFolders(opened.storage);
275
- return {
276
- json: { folders },
277
- text: folders.length === 0
278
- ? "(no folders)"
279
- : folders.map((f) => `${f.id}\t${f.name}`).join("\n"),
280
- };
281
- }
282
- finally {
283
- await opened.close();
284
- }
285
- });
286
- });
287
- folder
288
- .command("join <folderId>")
289
- .description("Accept a pending folder invitation (join the room)")
290
- .action(async (folderId, _opts, command) => {
291
- await runAction(command, async () => {
292
- const opened = await openStorage();
293
- try {
294
- const result = await core.joinFolder(opened.storage, folderId);
295
- return { json: { ...result }, text: `Joined folder ${result.folderId}` };
296
- }
297
- finally {
298
- await opened.close();
299
- }
300
- });
301
- });
302
- folder
303
- .command("share <folderId> <userId>")
304
- .description("Invite a participant to a shared folder at a given role")
305
- .option("--role <role>", "viewer or editor", "viewer")
306
- .action(async (folderId, userId, opts, command) => {
307
- await runAction(command, async () => {
308
- // Validated here too (before openStorage/login is even attempted) so
309
- // a bad --role fails fast exactly as before; core.shareFolder repeats
310
- // the same check so it's still safe to call standalone (e.g. from a
311
- // future UI) without this CLI-side pre-check.
312
- if (opts.role !== "viewer" && opts.role !== "editor") {
313
- throw new CliError(`invalid --role "${opts.role}" (must be viewer or editor)`);
314
- }
315
- const opened = await openStorage();
316
- try {
317
- const result = await core.shareFolder(opened.storage, folderId, userId, opts.role);
318
- return {
319
- json: { ...result },
320
- text: `Invited ${result.userId} to ${result.folderId} as ${result.role}`,
321
- };
322
- }
323
- finally {
324
- await opened.close();
325
- }
326
- });
327
- });
328
- folder
329
- .command("members <folderId>")
330
- .description("List participants and their roles")
331
- .action(async (folderId, _opts, command) => {
332
- await runAction(command, async () => {
333
- const opened = await openStorage();
334
- try {
335
- const members = await core.listMembers(opened.storage, folderId);
336
- return {
337
- json: { members },
338
- text: members.map((m) => `${m.userId}\t${m.role}\t${m.membership}`).join("\n"),
339
- };
340
- }
341
- finally {
342
- await opened.close();
343
- }
344
- });
345
- });
346
- folder
347
- .command("unshare <folderId> <userId>")
348
- .description("Remove a participant from a shared folder")
349
- .action(async (folderId, userId, _opts, command) => {
350
- await runAction(command, async () => {
351
- const opened = await openStorage();
352
- try {
353
- const result = await core.unshareFolder(opened.storage, folderId, userId);
354
- return { json: { ...result }, text: `Removed ${result.userId} from ${result.folderId}` };
355
- }
356
- finally {
357
- await opened.close();
358
- }
359
- });
360
- });
361
- // ---------------------------------------------------------------------------
362
- // Files
363
- // ---------------------------------------------------------------------------
364
- const file = storage.command("file").description("File operations within a folder");
365
- file
366
- .command("upload <folderId> <path>")
367
- .description("Encrypt and upload a local file into a folder")
368
- .option("--name <name>", "Name to store the file as (default: basename of path)")
369
- .action(async (folderId, filePath, opts, command) => {
370
- await runAction(command, async () => {
371
- if (!fs.existsSync(filePath)) {
372
- throw new CliError(`file not found: ${filePath}`);
373
- }
374
- const opened = await openStorage();
375
- try {
376
- const name = opts.name ?? path.basename(filePath);
377
- const data = fs.readFileSync(filePath);
378
- const result = await core.uploadFile(opened.storage, folderId, name, data, guessMimetype(filePath));
379
- // If recovery/backup is already active for this account, give the
380
- // new session's key a chance to actually reach the server backup
381
- // before this short-lived process exits.
382
- await waitForBackupSettled(opened.storage);
383
- return {
384
- json: { fileId: result.id, name: result.name },
385
- text: `Uploaded "${result.name}" as ${result.id}`,
386
- };
387
- }
388
- finally {
389
- await opened.close();
390
- }
391
- });
392
- });
393
- file
394
- .command("list <folderId>")
395
- .description("List files in a folder")
396
- .action(async (folderId, _opts, command) => {
397
- await runAction(command, async () => {
398
- const opened = await openStorage();
399
- try {
400
- const files = await core.listFiles(opened.storage, folderId);
401
- return {
402
- json: { files },
403
- text: files.length === 0 ? "(no files)" : files.map((f) => `${f.id}\t${f.name}`).join("\n"),
404
- };
405
- }
406
- finally {
407
- await opened.close();
408
- }
409
- });
410
- });
411
- file
412
- .command("download <folderId> <fileId> <destPath>")
413
- .description("Download and decrypt a file to a local path")
414
- .action(async (folderId, fileId, destPath, _opts, command) => {
415
- await runAction(command, async () => {
416
- const opened = await openStorage();
417
- try {
418
- const result = await core.downloadFile(opened.storage, folderId, fileId);
419
- fs.writeFileSync(destPath, Buffer.from(result.bytes));
420
- return {
421
- json: { path: destPath, bytes: result.bytes.byteLength, mimetype: result.mimetype },
422
- text: `Downloaded ${result.bytes.byteLength} bytes to ${destPath}`,
423
- };
424
- }
425
- finally {
426
- await opened.close();
427
- }
428
- });
429
- });
430
- // ---------------------------------------------------------------------------
431
- program
432
- .parseAsync(process.argv)
433
- .catch((err) => {
434
- process.stderr.write(`Error: ${err.message}\n`);
435
- process.exitCode = 1;
436
- })
437
- .finally(() => {
438
- // Every action closes its own storage (stopClient()), but Node's global
439
- // fetch (undici) can keep a keep-alive socket open past that, leaving
440
- // the process hanging instead of exiting on its own. Exit explicitly
441
- // once the command has fully finished and printed its output.
442
- process.exit(process.exitCode ?? 0);
443
- });
@@ -1,19 +0,0 @@
1
- import type { Session } from "./profile.js";
2
- export interface DeviceCodeLoginHooks {
3
- /** Called once the device+user code is known, before polling begins —
4
- * the caller (CLI command) prints it and attempts to open the browser. */
5
- onVerification: (info: {
6
- verificationUri: string;
7
- verificationUriComplete?: string;
8
- userCode: string;
9
- }) => void;
10
- }
11
- /**
12
- * Runs the full device-code login flow against `homeserver`: discovery, DCR,
13
- * start device authorization, print verification info + try to open the
14
- * browser, poll until approved, confirm identity via `/whoami`. Returns a
15
- * `Session` ready to `writeSession()` — includes the OIDC token set fields
16
- * (`refreshToken`, `oidcIssuer`, `oidcClientId`, `oidcIdToken`) so later CLI
17
- * invocations can reuse + refresh it.
18
- */
19
- export declare function runDeviceCodeLogin(homeserver: string, hooks: DeviceCodeLoginHooks): Promise<Session>;
package/dist/cli/oidc.js DELETED
@@ -1,97 +0,0 @@
1
- /**
2
- * CLI-only OIDC/MAS adapter: device-code login (RFC 8628) against a
3
- * homeserver's delegated auth service (MAS). Node-only (child_process for
4
- * best-effort browser open). Platform-specific behavior stays out of `src/core/`; the actual
5
- * OIDC protocol calls all live in `src/core/oidc.ts` and are shared with the
6
- * UI's PKCE adapter.
7
- */
8
- import { randomBytes } from "node:crypto";
9
- import { spawn } from "node:child_process";
10
- import { discoverOidcIssuer, registerClient, startDeviceCodeLogin, waitForDeviceCodeLogin, isDeviceAccessTokenError, whoAmI, } from "../core/oidc.js";
11
- import { CliError } from "./errors.js";
12
- import { withOidcWindowShim } from "./oidcWindowPolyfill.js";
13
- /** Generates a device ID the same shape matrix-js-sdk itself would (short
14
- * uppercase alphanumeric) — this CLI process chooses it upfront (unlike the
15
- * UI's authorization-code flow, where the SDK picks a random one) so it's
16
- * available immediately for logging/display and is guaranteed to match the
17
- * resulting Matrix device_id (embedded in the requested scope, see
18
- * `core/oidc.ts`'s `startDeviceCodeLogin`). */
19
- function generateDeviceId() {
20
- return randomBytes(5).toString("hex").toUpperCase();
21
- }
22
- /** Best-effort: try to open the verification URL in the user's default
23
- * browser. Never throws — if it fails (headless box, no display, unknown
24
- * platform), the caller already printed the URL for the user to open by
25
- * hand. */
26
- function tryOpenBrowser(url) {
27
- const platform = process.platform;
28
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
29
- try {
30
- const child = spawn(cmd, platform === "win32" ? ["", url] : [url], {
31
- detached: true,
32
- stdio: "ignore",
33
- shell: platform === "win32",
34
- });
35
- child.unref();
36
- }
37
- catch {
38
- // best-effort only
39
- }
40
- }
41
- /**
42
- * Runs the full device-code login flow against `homeserver`: discovery, DCR,
43
- * start device authorization, print verification info + try to open the
44
- * browser, poll until approved, confirm identity via `/whoami`. Returns a
45
- * `Session` ready to `writeSession()` — includes the OIDC token set fields
46
- * (`refreshToken`, `oidcIssuer`, `oidcClientId`, `oidcIdToken`) so later CLI
47
- * invocations can reuse + refresh it.
48
- */
49
- export async function runDeviceCodeLogin(homeserver, hooks) {
50
- // See src/cli/oidcWindowPolyfill.ts: discovery is the one OIDC call that
51
- // needs a `window` stub under Node, and the only place in the CLI it's
52
- // safe to install one — nothing crypto/WASM-related exists yet in this
53
- // process.
54
- const authMetadata = await withOidcWindowShim(() => discoverOidcIssuer(homeserver));
55
- if (!authMetadata.device_authorization_endpoint) {
56
- throw new CliError(`${homeserver} does not advertise OIDC device-code support (no device_authorization_endpoint) — try password login instead.`);
57
- }
58
- const clientId = await registerClient(authMetadata, {
59
- clientName: "TeleCrypt.io CLI",
60
- clientUri: "https://telecrypt.io/",
61
- applicationType: "native",
62
- // Device-code flow never redirects a browser back to us, so this URI is
63
- // never actually dereferenced — it's a DCR-schema placeholder only.
64
- // Unverified against production MAS's DCR policy (only exercised here
65
- // against the local dev/test MAS, which allows insecure/mismatched URIs).
66
- redirectUris: ["http://localhost:0/"],
67
- contacts: undefined,
68
- tosUri: undefined,
69
- policyUri: undefined,
70
- });
71
- const deviceId = generateDeviceId();
72
- const session = await startDeviceCodeLogin(authMetadata, clientId, deviceId);
73
- hooks.onVerification({
74
- verificationUri: session.verification_uri,
75
- verificationUriComplete: session.verification_uri_complete,
76
- userCode: session.user_code,
77
- });
78
- tryOpenBrowser(session.verification_uri_complete ?? session.verification_uri);
79
- const result = await waitForDeviceCodeLogin(authMetadata, clientId, session);
80
- if (isDeviceAccessTokenError(result)) {
81
- throw new CliError(`device login was not approved: ${result.error_description ?? result.error}`);
82
- }
83
- const who = await whoAmI(homeserver, result.access_token);
84
- if (who.deviceId && who.deviceId !== deviceId) {
85
- throw new CliError(`device_id mismatch after OIDC login: requested ${deviceId}, server confirmed ${who.deviceId}`);
86
- }
87
- return {
88
- homeserver,
89
- userId: who.userId,
90
- deviceId,
91
- accessToken: result.access_token,
92
- refreshToken: result.refresh_token,
93
- oidcIssuer: authMetadata.issuer,
94
- oidcClientId: clientId,
95
- oidcTokenEndpoint: authMetadata.token_endpoint,
96
- };
97
- }
@@ -1 +0,0 @@
1
- export declare function withOidcWindowShim<T>(fn: () => Promise<T>): Promise<T>;
@@ -1,76 +0,0 @@
1
- /**
2
- * `src/core/oidc.ts`'s `discoverOidcIssuer()` calls matrix-js-sdk's
3
- * `MatrixClient.getAuthMetadata()`, which internally constructs
4
- * oidc-client-ts state requiring `window.sessionStorage`/`window.
5
- * localStorage` to exist — a real browser has both natively; plain Node does
6
- * not, so under Node it throws `ReferenceError: window is not defined`
7
- * before ever making the HTTP request. Confirmed by direct testing — this is
8
- * a genuine gap in matrix-js-sdk's OIDC helpers for non-browser consumers,
9
- * not a misconfiguration on our side. (The same root cause is why we
10
- * deliberately do NOT use matrix-js-sdk's `OidcTokenRefresher` for token
11
- * refresh — see `core/oidc.ts`'s `refreshOidcToken`/`buildTokenRefreshFunction`,
12
- * a plain hand-rolled `fetch` instead, which needs no `window` at all and so
13
- * has no such gap, no matter when a refresh happens to fire.)
14
- *
15
- * `withOidcWindowShim()` installs a minimal in-memory `Storage`-shaped
16
- * `window` stub, runs `fn`, then removes it again — DELIBERATELY SCOPED, not
17
- * a permanent global assignment:
18
- *
19
- * A permanent `globalThis.window` breaks something unrelated —
20
- * `@matrix-org/matrix-sdk-crypto-wasm`'s own environment detection
21
- * (`typeof window !== "undefined"` → assumes a real browser IndexedDB is
22
- * available) fails with "Unsupported environment" once `window` exists at
23
- * all, even as an empty-ish stub, since CLI crypto persistence depends on Node being detected as Node
24
- * (`fake-indexeddb`), not as a browser. Confirmed by direct testing:
25
- * `initRustCrypto()` broke the moment `globalThis.window` was defined
26
- * unconditionally at module load.
27
- *
28
- * This is safe to use ONLY around `discoverOidcIssuer()`, and ONLY at OIDC
29
- * login time (`src/cli/oidc.ts`'s `runDeviceCodeLogin`) — the one call site
30
- * where it runs before any `TeleCryptIOStorage`/`MatrixClient`/rust-crypto
31
- * WASM exists in the process, so there is no concurrent WASM environment
32
- * check for the temporary stub to corrupt. Never wrap anything that runs
33
- * concurrently with, or after, crypto initialisation.
34
- */
35
- class MemoryStorage {
36
- map = new Map();
37
- get length() {
38
- return this.map.size;
39
- }
40
- clear() {
41
- this.map.clear();
42
- }
43
- getItem(key) {
44
- return this.map.has(key) ? this.map.get(key) : null;
45
- }
46
- key(index) {
47
- return [...this.map.keys()][index] ?? null;
48
- }
49
- removeItem(key) {
50
- this.map.delete(key);
51
- }
52
- setItem(key, value) {
53
- this.map.set(key, value);
54
- }
55
- }
56
- export async function withOidcWindowShim(fn) {
57
- const hadWindow = "window" in globalThis;
58
- const previous = globalThis.window;
59
- if (!hadWindow) {
60
- globalThis.window = {
61
- sessionStorage: new MemoryStorage(),
62
- localStorage: new MemoryStorage(),
63
- };
64
- }
65
- try {
66
- return await fn();
67
- }
68
- finally {
69
- if (!hadWindow) {
70
- delete globalThis.window;
71
- }
72
- else {
73
- globalThis.window = previous;
74
- }
75
- }
76
- }
@@ -1,16 +0,0 @@
1
- import type { Command } from "commander";
2
- export interface CommandResult {
3
- /** Machine-readable payload for --json. */
4
- json: Record<string, unknown>;
5
- /** Human-readable text for the default (non --json) output. */
6
- text: string;
7
- }
8
- /**
9
- * Runs a command action: executes `fn`, prints its result (JSON or human
10
- * text) to stdout on success, or a clean `{ "error": "..." }` (JSON mode) /
11
- * `Error: ...` (text mode) to stderr on failure — never a raw stack trace.
12
- * Sets process.exitCode accordingly (0 on success, 1 on failure) rather than
13
- * calling process.exit() directly, so callers can do any final cleanup
14
- * first.
15
- */
16
- export declare function runAction(command: Command, fn: () => Promise<CommandResult>): Promise<void>;
@@ -1,24 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- export { waitForCondition } from "../core/poll.js";