diff --git a/extensions/matrix/scripts/live-basic-send.ts b/extensions/matrix/scripts/live-basic-send.ts new file mode 100644 index 00000000000..f0469748c50 --- /dev/null +++ b/extensions/matrix/scripts/live-basic-send.ts @@ -0,0 +1,105 @@ +import { sendMatrixMessage } from "../src/matrix/actions.js"; +import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js"; +import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js"; + +async function main() { + const base = resolveLiveHarnessConfig(); + const pluginCfg = installLiveHarnessRuntime(base); + + const auth = await resolveMatrixAuth({ cfg: pluginCfg as never }); + const client = await createMatrixClient({ + homeserver: auth.homeserver, + userId: auth.userId, + accessToken: auth.accessToken, + password: auth.password, + deviceId: auth.deviceId, + encryption: false, + accountId: auth.accountId, + }); + + const targetUserId = process.argv[2]?.trim() || "@user:example.org"; + const stamp = new Date().toISOString(); + + try { + const dmRoomCreate = (await client.doRequest( + "POST", + "/_matrix/client/v3/createRoom", + undefined, + { + is_direct: true, + invite: [targetUserId], + preset: "trusted_private_chat", + name: `OpenClaw DM Test ${stamp}`, + topic: "matrix basic DM messaging test", + }, + )) as { room_id?: string }; + + const dmRoomId = dmRoomCreate.room_id?.trim() ?? ""; + if (!dmRoomId) { + throw new Error("Failed to create DM room"); + } + + const currentDirect = ((await client.getAccountData("m.direct").catch(() => ({}))) ?? + {}) as Record; + const existing = Array.isArray(currentDirect[targetUserId]) ? currentDirect[targetUserId] : []; + await client.setAccountData("m.direct", { + ...currentDirect, + [targetUserId]: [dmRoomId, ...existing.filter((id) => id !== dmRoomId)], + }); + + const dmByUserTarget = await sendMatrixMessage( + targetUserId, + `Matrix basic DM test (user target) ${stamp}`, + { client }, + ); + const dmByRoomTarget = await sendMatrixMessage( + dmRoomId, + `Matrix basic DM test (room target) ${stamp}`, + { client }, + ); + + const roomCreate = (await client.doRequest("POST", "/_matrix/client/v3/createRoom", undefined, { + invite: [targetUserId], + preset: "private_chat", + name: `OpenClaw Room Test ${stamp}`, + topic: "matrix basic room messaging test", + })) as { room_id?: string }; + + const roomId = roomCreate.room_id?.trim() ?? ""; + if (!roomId) { + throw new Error("Failed to create room chat room"); + } + + const roomSend = await sendMatrixMessage(roomId, `Matrix basic room test ${stamp}`, { + client, + }); + + process.stdout.write( + `${JSON.stringify( + { + homeserver: base.homeserver, + senderUserId: base.userId, + targetUserId, + dm: { + roomId: dmRoomId, + userTargetMessageId: dmByUserTarget.messageId, + roomTargetMessageId: dmByRoomTarget.messageId, + }, + room: { + roomId, + messageId: roomSend.messageId, + }, + }, + null, + 2, + )}\n`, + ); + } finally { + client.stop(); + } +} + +main().catch((err) => { + process.stderr.write(`BASIC_SEND_ERROR: ${err instanceof Error ? err.message : String(err)}\n`); + process.exit(1); +}); diff --git a/extensions/matrix/scripts/live-common.ts b/extensions/matrix/scripts/live-common.ts new file mode 100644 index 00000000000..50333ad7d0a --- /dev/null +++ b/extensions/matrix/scripts/live-common.ts @@ -0,0 +1,145 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { setMatrixRuntime } from "../src/runtime.js"; + +type EnvMap = Record; + +function loadEnvFile(filePath: string): EnvMap { + const out: EnvMap = {}; + if (!fs.existsSync(filePath)) { + return out; + } + const raw = fs.readFileSync(filePath, "utf8"); + for (const lineRaw of raw.split(/\r?\n/)) { + const line = lineRaw.trim(); + if (!line || line.startsWith("#")) { + continue; + } + const idx = line.indexOf("="); + if (idx <= 0) { + continue; + } + const key = line.slice(0, idx).trim(); + let value = line.slice(idx + 1).trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + out[key] = value; + } + return out; +} + +function normalizeHomeserver(raw: string): string { + const trimmed = raw.trim(); + if (!trimmed) { + return ""; + } + return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`; +} + +function chunkText(text: string, limit: number): string[] { + if (!text) { + return []; + } + if (text.length <= limit) { + return [text]; + } + const out: string[] = []; + for (let i = 0; i < text.length; i += limit) { + out.push(text.slice(i, i + limit)); + } + return out; +} + +export type LiveHarnessConfig = { + homeserver: string; + userId: string; + password: string; +}; + +export function resolveLiveHarnessConfig(): LiveHarnessConfig { + const envFromFile = loadEnvFile(path.join(os.homedir(), ".openclaw", ".env")); + const homeserver = normalizeHomeserver( + process.env.MATRIX_HOMESERVER ?? envFromFile.MATRIX_HOMESERVER ?? "", + ); + const userId = process.env.MATRIX_USER_ID ?? envFromFile.MATRIX_USER_ID ?? ""; + const password = process.env.MATRIX_PASSWORD ?? envFromFile.MATRIX_PASSWORD ?? ""; + + if (!homeserver || !userId || !password) { + throw new Error("Missing MATRIX_HOMESERVER / MATRIX_USER_ID / MATRIX_PASSWORD"); + } + + return { + homeserver, + userId, + password, + }; +} + +export function installLiveHarnessRuntime(cfg: LiveHarnessConfig): { + channels: { + matrix: { + homeserver: string; + userId: string; + password: string; + encryption: false; + }; + }; +} { + const pluginCfg = { + channels: { + matrix: { + homeserver: cfg.homeserver, + userId: cfg.userId, + password: cfg.password, + encryption: false as const, + }, + }, + }; + + setMatrixRuntime({ + config: { + loadConfig: () => pluginCfg, + }, + state: { + resolveStateDir: () => path.join(os.homedir(), ".openclaw", "matrix-live-harness-state"), + }, + channel: { + text: { + resolveMarkdownTableMode: () => "off", + convertMarkdownTables: (text: string) => text, + resolveTextChunkLimit: () => 4000, + resolveChunkMode: () => "off", + chunkMarkdownTextWithMode: (text: string, limit: number) => chunkText(text, limit), + }, + }, + media: { + mediaKindFromMime: (mime: string) => { + const value = (mime || "").toLowerCase(); + if (value.startsWith("image/")) { + return "image"; + } + if (value.startsWith("audio/")) { + return "audio"; + } + if (value.startsWith("video/")) { + return "video"; + } + return "document"; + }, + isVoiceCompatibleAudio: () => false, + loadWebMedia: async () => ({ + buffer: Buffer.from("matrix harness media payload\n", "utf8"), + contentType: "text/plain", + fileName: "matrix-harness.txt", + kind: "document" as const, + }), + }, + } as never); + + return pluginCfg; +} diff --git a/extensions/matrix/scripts/live-cross-signing-probe.ts b/extensions/matrix/scripts/live-cross-signing-probe.ts new file mode 100644 index 00000000000..3fbddaf0714 --- /dev/null +++ b/extensions/matrix/scripts/live-cross-signing-probe.ts @@ -0,0 +1,127 @@ +import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js"; +import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js"; + +type MatrixCryptoProbe = { + isCrossSigningReady?: () => Promise; + userHasCrossSigningKeys?: (userId?: string, downloadUncached?: boolean) => Promise; + bootstrapCrossSigning?: (opts: { + setupNewCrossSigning?: boolean; + authUploadDeviceSigningKeys?: ( + makeRequest: (authData: Record | null) => Promise, + ) => Promise; + }) => Promise; +}; + +async function main() { + const base = resolveLiveHarnessConfig(); + const cfg = installLiveHarnessRuntime(base); + (cfg.channels["matrix"] as { encryption: boolean }).encryption = true; + + const auth = await resolveMatrixAuth({ cfg: cfg as never }); + const client = await createMatrixClient({ + homeserver: auth.homeserver, + userId: auth.userId, + accessToken: auth.accessToken, + password: auth.password, + deviceId: auth.deviceId, + encryption: true, + accountId: auth.accountId, + }); + const initCrypto = (client as unknown as { initializeCryptoIfNeeded?: () => Promise }) + .initializeCryptoIfNeeded; + if (typeof initCrypto === "function") { + await initCrypto.call(client); + } + + const inner = (client as unknown as { client?: { getCrypto?: () => unknown } }).client; + const crypto = (inner?.getCrypto?.() ?? null) as MatrixCryptoProbe | null; + const userId = auth.userId; + const password = auth.password; + + const out: Record = { + userId, + hasCrypto: Boolean(crypto), + readyBefore: null, + hasKeysBefore: null, + bootstrap: "skipped", + readyAfter: null, + hasKeysAfter: null, + queryHasMaster: null, + queryHasSelfSigning: null, + queryHasUserSigning: null, + }; + + if (!crypto || !crypto.bootstrapCrossSigning) { + process.stdout.write(`${JSON.stringify(out, null, 2)}\n`); + return; + } + + if (typeof crypto.isCrossSigningReady === "function") { + out.readyBefore = await crypto.isCrossSigningReady().catch((err) => `error:${String(err)}`); + } + if (typeof crypto.userHasCrossSigningKeys === "function") { + out.hasKeysBefore = await crypto + .userHasCrossSigningKeys(userId, true) + .catch((err) => `error:${String(err)}`); + } + + const authUploadDeviceSigningKeys = async ( + makeRequest: (authData: Record | null) => Promise, + ): Promise => { + try { + return await makeRequest(null); + } catch { + try { + return await makeRequest({ type: "m.login.dummy" }); + } catch { + if (!password?.trim()) { + throw new Error("Missing password for m.login.password fallback"); + } + return await makeRequest({ + type: "m.login.password", + identifier: { type: "m.id.user", user: userId }, + password, + }); + } + } + }; + + try { + await crypto.bootstrapCrossSigning({ authUploadDeviceSigningKeys }); + out.bootstrap = "ok"; + } catch (err) { + out.bootstrap = "error"; + out.bootstrapError = err instanceof Error ? err.message : String(err); + } + + if (typeof crypto.isCrossSigningReady === "function") { + out.readyAfter = await crypto.isCrossSigningReady().catch((err) => `error:${String(err)}`); + } + if (typeof crypto.userHasCrossSigningKeys === "function") { + out.hasKeysAfter = await crypto + .userHasCrossSigningKeys(userId, true) + .catch((err) => `error:${String(err)}`); + } + + const query = (await client.doRequest("POST", "/_matrix/client/v3/keys/query", undefined, { + device_keys: { [userId]: [] }, + })) as { + master_keys?: Record; + self_signing_keys?: Record; + user_signing_keys?: Record; + }; + + out.queryHasMaster = Boolean(query.master_keys?.[userId]); + out.queryHasSelfSigning = Boolean(query.self_signing_keys?.[userId]); + out.queryHasUserSigning = Boolean(query.user_signing_keys?.[userId]); + + process.stdout.write(`${JSON.stringify(out, null, 2)}\n`); + client.stop(); +} + +main().catch((err) => { + process.stderr.write( + `CROSS_SIGNING_PROBE_ERROR: ${err instanceof Error ? err.message : String(err)}\n`, + ); + process.exit(1); +}); diff --git a/extensions/matrix/scripts/live-e2ee-bootstrap.ts b/extensions/matrix/scripts/live-e2ee-bootstrap.ts new file mode 100644 index 00000000000..a24fb3a071a --- /dev/null +++ b/extensions/matrix/scripts/live-e2ee-bootstrap.ts @@ -0,0 +1,28 @@ +import { bootstrapMatrixVerification } from "../src/matrix/actions/verification.js"; +import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js"; + +async function main() { + const recoveryKeyArg = process.argv[2]; + const forceResetCrossSigning = process.argv.includes("--force-reset-cross-signing"); + + const base = resolveLiveHarnessConfig(); + const pluginCfg = installLiveHarnessRuntime(base); + (pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true; + + const result = await bootstrapMatrixVerification({ + recoveryKey: recoveryKeyArg?.trim() || undefined, + forceResetCrossSigning, + }); + + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + if (!result.success) { + process.exitCode = 1; + } +} + +main().catch((err) => { + process.stderr.write( + `E2EE_BOOTSTRAP_ERROR: ${err instanceof Error ? err.message : String(err)}\n`, + ); + process.exit(1); +}); diff --git a/extensions/matrix/scripts/live-e2ee-room-state.ts b/extensions/matrix/scripts/live-e2ee-room-state.ts new file mode 100644 index 00000000000..015febd7cf6 --- /dev/null +++ b/extensions/matrix/scripts/live-e2ee-room-state.ts @@ -0,0 +1,66 @@ +import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js"; +import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js"; + +async function main() { + const roomId = process.argv[2]?.trim(); + const eventId = process.argv[3]?.trim(); + + if (!roomId) { + throw new Error( + "Usage: node --import tsx extensions/matrix/scripts/live-e2ee-room-state.ts [eventId]", + ); + } + + const base = resolveLiveHarnessConfig(); + const pluginCfg = installLiveHarnessRuntime(base); + (pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true; + + const auth = await resolveMatrixAuth({ cfg: pluginCfg as never }); + const client = await createMatrixClient({ + homeserver: auth.homeserver, + userId: auth.userId, + accessToken: auth.accessToken, + password: auth.password, + deviceId: auth.deviceId, + encryption: false, + accountId: auth.accountId, + }); + + try { + const encryptionState = (await client.doRequest( + "GET", + `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/state/m.room.encryption/`, + )) as { algorithm?: string; rotation_period_ms?: number; rotation_period_msgs?: number }; + + let eventType: string | null = null; + if (eventId) { + const event = (await client.doRequest( + "GET", + `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(eventId)}`, + )) as { type?: string }; + eventType = event.type ?? null; + } + + process.stdout.write( + `${JSON.stringify( + { + roomId, + encryptionState, + eventId: eventId ?? null, + eventType, + }, + null, + 2, + )}\n`, + ); + } finally { + client.stop(); + } +} + +main().catch((err) => { + process.stderr.write( + `E2EE_ROOM_STATE_ERROR: ${err instanceof Error ? err.message : String(err)}\n`, + ); + process.exit(1); +}); diff --git a/extensions/matrix/scripts/live-e2ee-send-room.ts b/extensions/matrix/scripts/live-e2ee-send-room.ts new file mode 100644 index 00000000000..85e87e7a0d3 --- /dev/null +++ b/extensions/matrix/scripts/live-e2ee-send-room.ts @@ -0,0 +1,100 @@ +import { sendMatrixMessage } from "../src/matrix/actions.js"; +import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js"; +import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js"; + +async function delay(ms: number): Promise { + await new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function main() { + const roomId = process.argv[2]?.trim(); + const useFullBootstrap = process.argv.includes("--full-bootstrap"); + const startupTimeoutMs = 45_000; + const settleMsRaw = Number.parseInt(process.argv[3] ?? "4000", 10); + const settleMs = Number.isFinite(settleMsRaw) && settleMsRaw >= 0 ? settleMsRaw : 4000; + + if (!roomId) { + throw new Error( + "Usage: node --import tsx extensions/matrix/scripts/live-e2ee-send-room.ts [settleMs] [--full-bootstrap]", + ); + } + + const base = resolveLiveHarnessConfig(); + const pluginCfg = installLiveHarnessRuntime(base); + (pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true; + + const auth = await resolveMatrixAuth({ cfg: pluginCfg as never }); + const client = await createMatrixClient({ + homeserver: auth.homeserver, + userId: auth.userId, + accessToken: auth.accessToken, + password: auth.password, + deviceId: auth.deviceId, + encryption: true, + accountId: auth.accountId, + }); + + const stamp = new Date().toISOString(); + + try { + if (!useFullBootstrap) { + const bootstrapper = ( + client as unknown as { cryptoBootstrapper?: { bootstrap?: () => Promise } } + ).cryptoBootstrapper; + if (bootstrapper?.bootstrap) { + bootstrapper.bootstrap = async () => {}; + } + } + + await Promise.race([ + client.start(), + new Promise((_, reject) => { + setTimeout(() => { + reject( + new Error( + `Matrix client start timed out after ${startupTimeoutMs}ms (fullBootstrap=${useFullBootstrap})`, + ), + ); + }, startupTimeoutMs); + }), + ]); + + if (settleMs > 0) { + await delay(settleMs); + } + + const sent = await sendMatrixMessage( + roomId, + `Matrix E2EE existing-room test ${stamp} (settleMs=${settleMs})`, + { client }, + ); + + const event = (await client.doRequest( + "GET", + `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(sent.messageId)}`, + )) as { type?: string }; + + process.stdout.write( + `${JSON.stringify( + { + roomId, + messageId: sent.messageId, + storedEventType: event.type ?? null, + fullBootstrap: useFullBootstrap, + settleMs, + }, + null, + 2, + )}\n`, + ); + } finally { + client.stop(); + } +} + +main().catch((err) => { + process.stderr.write( + `E2EE_SEND_ROOM_ERROR: ${err instanceof Error ? err.message : String(err)}\n`, + ); + process.exit(1); +}); diff --git a/extensions/matrix/scripts/live-e2ee-send.ts b/extensions/matrix/scripts/live-e2ee-send.ts new file mode 100644 index 00000000000..6d6977622c9 --- /dev/null +++ b/extensions/matrix/scripts/live-e2ee-send.ts @@ -0,0 +1,170 @@ +import { sendMatrixMessage } from "../src/matrix/actions.js"; +import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js"; +import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js"; + +const MEGOLM_ALG = "m.megolm.v1.aes-sha2"; + +type MatrixEventLike = { + type?: string; +}; + +async function main() { + const targetUserId = process.argv[2]?.trim() || "@user:example.org"; + const useFullBootstrap = process.argv.includes("--full-bootstrap"); + const startupTimeoutMs = 45_000; + const base = resolveLiveHarnessConfig(); + const pluginCfg = installLiveHarnessRuntime(base); + + // Enable encryption for this run only. + (pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true; + + const auth = await resolveMatrixAuth({ cfg: pluginCfg as never }); + const client = await createMatrixClient({ + homeserver: auth.homeserver, + userId: auth.userId, + accessToken: auth.accessToken, + password: auth.password, + deviceId: auth.deviceId, + encryption: true, + accountId: auth.accountId, + }); + + const stamp = new Date().toISOString(); + + try { + if (!useFullBootstrap) { + const bootstrapper = ( + client as unknown as { cryptoBootstrapper?: { bootstrap?: () => Promise } } + ).cryptoBootstrapper; + if (bootstrapper?.bootstrap) { + bootstrapper.bootstrap = async () => {}; + } + } + + await Promise.race([ + client.start(), + new Promise((_, reject) => { + setTimeout(() => { + reject( + new Error( + `Matrix client start timed out after ${startupTimeoutMs}ms (fullBootstrap=${useFullBootstrap})`, + ), + ); + }, startupTimeoutMs); + }), + ]); + + const dmRoomCreate = (await client.doRequest( + "POST", + "/_matrix/client/v3/createRoom", + undefined, + { + is_direct: true, + invite: [targetUserId], + preset: "trusted_private_chat", + name: `OpenClaw E2EE DM ${stamp}`, + topic: "matrix E2EE DM test", + initial_state: [ + { + type: "m.room.encryption", + state_key: "", + content: { + algorithm: MEGOLM_ALG, + }, + }, + ], + }, + )) as { room_id?: string }; + + const dmRoomId = dmRoomCreate.room_id?.trim() ?? ""; + if (!dmRoomId) { + throw new Error("Failed to create encrypted DM room"); + } + + const currentDirect = ((await client.getAccountData("m.direct").catch(() => ({}))) ?? + {}) as Record; + const existing = Array.isArray(currentDirect[targetUserId]) ? currentDirect[targetUserId] : []; + await client.setAccountData("m.direct", { + ...currentDirect, + [targetUserId]: [dmRoomId, ...existing.filter((id) => id !== dmRoomId)], + }); + + const dmSend = await sendMatrixMessage( + dmRoomId, + `Matrix E2EE DM test ${stamp}\nPlease reply here so I can validate decrypt/read.`, + { + client, + }, + ); + + const roomCreate = (await client.doRequest("POST", "/_matrix/client/v3/createRoom", undefined, { + invite: [targetUserId], + preset: "private_chat", + name: `OpenClaw E2EE Room ${stamp}`, + topic: "matrix E2EE room test", + initial_state: [ + { + type: "m.room.encryption", + state_key: "", + content: { + algorithm: MEGOLM_ALG, + }, + }, + ], + })) as { room_id?: string }; + + const roomId = roomCreate.room_id?.trim() ?? ""; + if (!roomId) { + throw new Error("Failed to create encrypted room chat"); + } + + const roomSend = await sendMatrixMessage( + roomId, + `Matrix E2EE room test ${stamp}\nPlease reply here too.`, + { + client, + }, + ); + + const dmRaw = (await client.doRequest( + "GET", + `/_matrix/client/v3/rooms/${encodeURIComponent(dmRoomId)}/event/${encodeURIComponent(dmSend.messageId)}`, + )) as MatrixEventLike; + + const roomRaw = (await client.doRequest( + "GET", + `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(roomSend.messageId)}`, + )) as MatrixEventLike; + + process.stdout.write( + `${JSON.stringify( + { + homeserver: base.homeserver, + senderUserId: base.userId, + targetUserId, + encryptionAlgorithm: MEGOLM_ALG, + fullBootstrap: useFullBootstrap, + dm: { + roomId: dmRoomId, + messageId: dmSend.messageId, + storedEventType: dmRaw.type ?? null, + }, + room: { + roomId, + messageId: roomSend.messageId, + storedEventType: roomRaw.type ?? null, + }, + }, + null, + 2, + )}\n`, + ); + } finally { + client.stop(); + } +} + +main().catch((err) => { + process.stderr.write(`E2EE_SEND_ERROR: ${err instanceof Error ? err.message : String(err)}\n`); + process.exit(1); +}); diff --git a/extensions/matrix/scripts/live-e2ee-status.ts b/extensions/matrix/scripts/live-e2ee-status.ts new file mode 100644 index 00000000000..520d001bc84 --- /dev/null +++ b/extensions/matrix/scripts/live-e2ee-status.ts @@ -0,0 +1,57 @@ +import { + getMatrixEncryptionStatus, + getMatrixVerificationStatus, + verifyMatrixRecoveryKey, +} from "../src/matrix/actions.js"; +import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js"; + +async function main() { + const includeRecoveryKey = process.argv.includes("--include-recovery-key"); + const verifyStoredRecoveryKey = process.argv.includes("--verify-stored-recovery-key"); + + const base = resolveLiveHarnessConfig(); + const pluginCfg = installLiveHarnessRuntime(base); + (pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true; + + const verification = await getMatrixVerificationStatus({ + includeRecoveryKey, + }); + const encryption = await getMatrixEncryptionStatus({ + includeRecoveryKey, + }); + + let recoveryVerificationResult: unknown = null; + if (verifyStoredRecoveryKey) { + const key = + verification && typeof verification === "object" && "recoveryKey" in verification + ? (verification as { recoveryKey?: string | null }).recoveryKey + : null; + if (key?.trim()) { + recoveryVerificationResult = await verifyMatrixRecoveryKey(key); + } else { + recoveryVerificationResult = { + success: false, + error: "No stored recovery key returned (use --include-recovery-key)", + }; + } + } + + process.stdout.write( + `${JSON.stringify( + { + homeserver: base.homeserver, + userId: base.userId, + verification, + encryption, + recoveryVerificationResult, + }, + null, + 2, + )}\n`, + ); +} + +main().catch((err) => { + process.stderr.write(`E2EE_STATUS_ERROR: ${err instanceof Error ? err.message : String(err)}\n`); + process.exit(1); +}); diff --git a/extensions/matrix/scripts/live-e2ee-wait-reply.ts b/extensions/matrix/scripts/live-e2ee-wait-reply.ts new file mode 100644 index 00000000000..cf415bd1ede --- /dev/null +++ b/extensions/matrix/scripts/live-e2ee-wait-reply.ts @@ -0,0 +1,123 @@ +import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js"; +import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js"; + +type MatrixRawEvent = { + event_id?: string; + type?: string; + sender?: string; + room_id?: string; + origin_server_ts?: number; + content?: { + body?: string; + msgtype?: string; + }; +}; + +async function main() { + const roomId = process.argv[2]?.trim(); + const targetUserId = process.argv[3]?.trim() || "@user:example.org"; + const timeoutSecRaw = Number.parseInt(process.argv[4] ?? "120", 10); + const timeoutMs = + (Number.isFinite(timeoutSecRaw) && timeoutSecRaw > 0 ? timeoutSecRaw : 120) * 1000; + const useFullBootstrap = process.argv.includes("--full-bootstrap"); + const startupTimeoutMs = 45_000; + + if (!roomId) { + throw new Error( + "Usage: node --import tsx extensions/matrix/scripts/live-e2ee-wait-reply.ts [targetUserId] [timeoutSec] [--full-bootstrap]", + ); + } + + const base = resolveLiveHarnessConfig(); + const pluginCfg = installLiveHarnessRuntime(base); + (pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true; + + const auth = await resolveMatrixAuth({ cfg: pluginCfg as never }); + const client = await createMatrixClient({ + homeserver: auth.homeserver, + userId: auth.userId, + accessToken: auth.accessToken, + password: auth.password, + deviceId: auth.deviceId, + encryption: true, + accountId: auth.accountId, + }); + + try { + if (!useFullBootstrap) { + const bootstrapper = ( + client as unknown as { cryptoBootstrapper?: { bootstrap?: () => Promise } } + ).cryptoBootstrapper; + if (bootstrapper?.bootstrap) { + bootstrapper.bootstrap = async () => {}; + } + } + + await Promise.race([ + client.start(), + new Promise((_, reject) => { + setTimeout(() => { + reject( + new Error( + `Matrix client start timed out after ${startupTimeoutMs}ms (fullBootstrap=${useFullBootstrap})`, + ), + ); + }, startupTimeoutMs); + }), + ]); + + const found = await new Promise((resolve) => { + const timer = setTimeout(() => { + resolve(null); + }, timeoutMs); + + client.on("room.message", (eventRoomId, event) => { + const rid = String(eventRoomId || ""); + const raw = event as MatrixRawEvent; + if (rid !== roomId) { + return; + } + if ((raw.sender ?? "").trim() !== targetUserId) { + return; + } + if ((raw.type ?? "").trim() !== "m.room.message") { + return; + } + clearTimeout(timer); + resolve(raw); + }); + }); + + process.stdout.write( + `${JSON.stringify( + { + roomId, + targetUserId, + timeoutMs, + found: Boolean(found), + message: found + ? { + eventId: found.event_id ?? null, + type: found.type ?? null, + sender: found.sender ?? null, + timestamp: found.origin_server_ts ?? null, + text: found.content?.body ?? null, + msgtype: found.content?.msgtype ?? null, + } + : null, + }, + null, + 2, + )}\n`, + ); + } finally { + client.stop(); + } +} + +main().catch((err) => { + process.stderr.write( + `E2EE_WAIT_REPLY_ERROR: ${err instanceof Error ? err.message : String(err)}\n`, + ); + process.exit(1); +}); diff --git a/extensions/matrix/scripts/live-read-room.ts b/extensions/matrix/scripts/live-read-room.ts new file mode 100644 index 00000000000..2bf3df85d09 --- /dev/null +++ b/extensions/matrix/scripts/live-read-room.ts @@ -0,0 +1,57 @@ +import { readMatrixMessages } from "../src/matrix/actions.js"; +import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js"; +import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js"; + +async function main() { + const roomId = process.argv[2]?.trim(); + if (!roomId) { + throw new Error("Usage: bun extensions/matrix/scripts/live-read-room.ts [limit]"); + } + + const requestedLimit = Number.parseInt(process.argv[3] ?? "30", 10); + const limit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? requestedLimit : 30; + + const base = resolveLiveHarnessConfig(); + const pluginCfg = installLiveHarnessRuntime(base); + const auth = await resolveMatrixAuth({ cfg: pluginCfg as never }); + const client = await createMatrixClient({ + homeserver: auth.homeserver, + userId: auth.userId, + accessToken: auth.accessToken, + password: auth.password, + deviceId: auth.deviceId, + encryption: false, + accountId: auth.accountId, + }); + + try { + const result = await readMatrixMessages(roomId, { client, limit }); + const compact = result.messages.map((msg) => ({ + id: msg.eventId, + sender: msg.sender, + ts: msg.timestamp, + text: msg.body ?? "", + })); + + process.stdout.write( + `${JSON.stringify( + { + roomId, + count: compact.length, + messages: compact, + nextBatch: result.nextBatch ?? null, + prevBatch: result.prevBatch ?? null, + }, + null, + 2, + )}\n`, + ); + } finally { + client.stop(); + } +} + +main().catch((err) => { + process.stderr.write(`READ_ROOM_ERROR: ${err instanceof Error ? err.message : String(err)}\n`); + process.exit(1); +});