- Image compression in an encrypted room re-encoded the *plaintext* original, uploaded it unencrypted, and reused the original's encInfo, so the media sat on the server in the clear AND the attachment was undecryptable. The compressed bytes are now run through encryptFile and the synthetic upload item carries the new encInfo (buildCompressedUploadItem, unit-tested; it can never inherit the stale encInfo). - Scheduled messages (MSC4140) are PUT as raw m.room.message, bypassing the SDK encryption pipeline. The Schedule button is now hidden in encrypted rooms, handleScheduleClick no-ops there, and scheduleMessage() itself refuses with a clear error so no caller can regress this. README notes the limitation. - The GIF picker uploaded the Giphy blob unencrypted into E2EE rooms; it now mirrors the voice/attachment path (encryptFile -> upload ciphertext -> content.file). Fixes #6 Fixes #7 Fixes #11 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
50 lines
1.6 KiB
TypeScript
50 lines
1.6 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import { scheduleMessage } from './scheduledMessages';
|
|
|
|
// MSC4140 delayed events are PUT as a plaintext m.room.message, so scheduling
|
|
// must be refused outright for encrypted rooms — the composer hides the button,
|
|
// this guard stops any other caller from regressing it.
|
|
const makeMx = (encrypted: boolean | undefined) => {
|
|
const calls: unknown[][] = [];
|
|
const mx = {
|
|
getRoom: (_roomId: string) =>
|
|
encrypted === undefined ? null : { hasEncryptionStateEvent: () => encrypted },
|
|
http: {
|
|
authedRequest: (...args: unknown[]) => {
|
|
calls.push(args);
|
|
return Promise.resolve({ delay_id: 'delay-1' });
|
|
},
|
|
},
|
|
};
|
|
return { mx: mx as never, calls };
|
|
};
|
|
|
|
test('scheduleMessage throws and sends nothing for an encrypted room', async () => {
|
|
const { mx, calls } = makeMx(true);
|
|
await assert.rejects(
|
|
() => scheduleMessage(mx, '!enc:lotusguild.org', { body: 'secret' }, Date.now() + 60_000),
|
|
/encrypted rooms/,
|
|
);
|
|
assert.equal(calls.length, 0);
|
|
});
|
|
|
|
test('scheduleMessage still sends for an unencrypted room', async () => {
|
|
const { mx, calls } = makeMx(false);
|
|
const delayId = await scheduleMessage(
|
|
mx,
|
|
'!plain:lotusguild.org',
|
|
{ body: 'hi' },
|
|
Date.now() + 60_000,
|
|
);
|
|
assert.equal(delayId, 'delay-1');
|
|
assert.equal(calls.length, 1);
|
|
});
|
|
|
|
test('scheduleMessage sends when the room is unknown to the client', async () => {
|
|
const { mx, calls } = makeMx(undefined);
|
|
await scheduleMessage(mx, '!unknown:lotusguild.org', { body: 'hi' }, Date.now() + 60_000);
|
|
assert.equal(calls.length, 1);
|
|
});
|