UNPKG

relay-companion

Version:

Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.

1,212 lines • 52.7 kB
import {
  createCipheriv,
  createDecipheriv,
  createHash,
  createPrivateKey,
  randomBytes,
  sign,
} from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import {
  createApplicationMessage,
  createCommit,
  createGroup,
  decodeMlsMessage,
  defaultCapabilities,
  emptyPskIndex,
  encodeMlsMessage,
  generateKeyPackageWithKey,
  getCiphersuiteFromName,
  getCiphersuiteImpl,
  joinGroup,
  processPrivateMessage,
  zeroOutUint8Array,
} from "ts-mls";
import { defaultClientConfig } from "ts-mls/clientConfig.js";
import { decodeRatchetTree, encodeRatchetTree } from "ts-mls/ratchetTree.js";
import e2eeIdentityModule from "./e2ee-identity.cjs";
import {
  assertE2eeRecipientDevicesTrusted,
  assertE2eeSenderDeviceTrusted,
} from "./e2ee-device-trust.js";
import {
  activeDeviceFromTransparency,
  assertAndPinE2eeMode,
  assertKnownCheckpoint,
  cachePlaintext,
  canonicalKeyPackageProof,
  canonicalMlsCredential,
  completeE2eeOutbox,
  deviceAtTransparencyCheckpoint,
  e2eeRequestHash,
  mutateKeyPackageState,
  parseMlsCredential,
  readCachedPlaintext,
  readImportedE2eeHistoryRecord,
  readKeyPackageState,
  readPendingE2eeOutbox,
  syncTransparency,
  writePendingE2eeOutbox,
} from "./e2ee-state.js";

const {
  E2EE_CIPHER_SUITE,
  E2EE_PROTOCOL,
  identityPath,
  readPairedIdentity,
  verifyDeviceCrossSignature,
} = e2eeIdentityModule;

const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder("utf-8", { fatal: true });
const KEY_PACKAGE_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
const E2EE_ATTACHMENT_TOTAL_CIPHERTEXT_BYTES = 12 * 1024 * 1024;
const ATTACHMENT_FILE_CONTEXT = "relay-e2ee-attachment-file-v1";
const ATTACHMENT_METADATA_CONTEXT = "relay-e2ee-attachment-metadata-v1";

export function b64(value) {
  return Buffer.from(value).toString("base64url");
}

export function unb64(value) {
  return new Uint8Array(Buffer.from(String(value || ""), "base64url"));
}

export function decodeExact(decoder, value, label) {
  const bytes = unb64(value);
  const decoded = decoder(bytes, 0);
  if (!decoded || decoded[1] !== bytes.length) throw new Error(`Invalid ${label} encoding.`);
  return decoded[0];
}

export async function cipherSuite() {
  return getCiphersuiteImpl(getCiphersuiteFromName(E2EE_CIPHER_SUITE));
}

export function identityOrThrow() {
  const identity = readPairedIdentity();
  if (!identity) {
    throw new Error("This Companion has no enrolled E2EE identity. Re-pair it before encrypted messaging.");
  }
  return identity;
}

export function localE2eeIdentityAvailable() {
  return Boolean(readPairedIdentity());
}

function directConversationId(leftUserId, rightUserId) {
  const users = [...new Set([String(leftUserId), String(rightUserId)])].sort();
  return `econv_${createHash("sha256")
    .update("relay-e2ee-direct-conversation-v1")
    .update(users.join("\n"))
    .digest("hex")
    .slice(0, 32)}`;
}

const E2EE_TARGET_SURFACES = new Set(["codex", "claude_code", "claude_desktop", "claude_cowork"]);

function encryptedTargetSurfaces(value) {
  return [...new Set((Array.isArray(value) ? value : [])
    .map((item) => String(item || ""))
    .filter((item) => E2EE_TARGET_SURFACES.has(item)))].slice(0, 4);
}

/** Keep portable provenance inside the ciphertext without leaking a local cwd. */
function encryptedSource(source) {
  if (!source || typeof source !== "object") return null;
  const result = { host: String(source.host || "unknown") };
  if (E2EE_TARGET_SURFACES.has(String(source.surface || ""))) result.surface = String(source.surface);
  if (source.workspace && typeof source.workspace === "object") {
    const workspace = {
      kind: String(source.workspace.kind || ""),
      key: String(source.workspace.key || ""),
      ...(source.workspace.label ? { label: String(source.workspace.label) } : {}),
      ...(source.workspace.branch ? { branch: String(source.workspace.branch) } : {}),
      ...(source.workspace.rootCommit ? { rootCommit: String(source.workspace.rootCommit) } : {}),
    };
    if (["git", "name"].includes(workspace.kind) && workspace.key) result.workspace = workspace;
  }
  if (source.repo && typeof source.repo === "object") {
    const repo = Object.fromEntries(["name", "originKey", "branch", "rootCommit"]
      .filter((key) => source.repo[key])
      .map((key) => [key, String(source.repo[key])]));
    if (Object.keys(repo).length) result.repo = repo;
  }
  return result;
}

async function encryptedRecord(client, relayId) {
  const identity = identityOrThrow();
  const imported = readImportedE2eeHistoryRecord(identity, relayId);
  if (imported) return imported;
  const result = await client.e2eeFetchMessages([relayId]);
  const wire = result?.packets?.[relayId];
  if (!wire) throw new Error("That encrypted message is not available to this device.");
  return { wire, plaintext: await decryptE2eeWireMessage(client, wire) };
}

function otherParticipant(identity, wire) {
  if (wire.sender.relayUserId === identity.userId) return wire.recipient.relayUserId;
  if (wire.recipient.relayUserId === identity.userId) return wire.sender.relayUserId;
  throw new Error("That encrypted message is not part of this account's conversation.");
}

export async function verifiedE2eeStatus(client) {
  const status = await client.e2eeStatus();
  const identity = readPairedIdentity();
  if (identity) assertAndPinE2eeMode(identity, status.mode);
  return status;
}

function credentialFor(identity) {
  return {
    credentialType: "basic",
    identity: textEncoder.encode(canonicalMlsCredential({
      relayUserId: identity.userId,
      deviceId: identity.deviceId,
      fingerprint: identity.fingerprint,
    })),
  };
}

function pinnedCapabilities() {
  return {
    ...defaultCapabilities(),
    versions: [E2EE_PROTOCOL],
    ciphersuites: [E2EE_CIPHER_SUITE],
    credentials: ["basic"],
  };
}

function packageLifetime() {
  const now = Math.floor(Date.now() / 1000);
  return { notBefore: BigInt(now - 60), notAfter: BigInt(now + KEY_PACKAGE_LIFETIME_SECONDS) };
}

function privatePackageToJson(value) {
  return {
    initPrivateKey: b64(value.initPrivateKey),
    hpkePrivateKey: b64(value.hpkePrivateKey),
    signaturePrivateKey: b64(value.signaturePrivateKey),
  };
}

export function privatePackageFromJson(value) {
  return {
    initPrivateKey: unb64(value.initPrivateKey),
    hpkePrivateKey: unb64(value.hpkePrivateKey),
    signaturePrivateKey: unb64(value.signaturePrivateKey),
  };
}

export async function generatePackage(identity, cs) {
  const lifetime = packageLifetime();
  const generated = await generateKeyPackageWithKey(
    credentialFor(identity),
    pinnedCapabilities(),
    lifetime,
    [],
    {
      signKey: unb64(identity.privateKeyJwk.d),
      publicKey: unb64(identity.signaturePublicKey),
    },
    cs,
  );
  const encoded = encodeMlsMessage({
    version: E2EE_PROTOCOL,
    wireformat: "mls_key_package",
    keyPackage: generated.publicPackage,
  });
  const packageRef = createHash("sha256").update(encoded).digest("base64url");
  const keyPackage = b64(encoded);
  const expiresAt = new Date(Number(lifetime.notAfter) * 1000).toISOString();
  const proof = sign(
    null,
    Buffer.from(canonicalKeyPackageProof({
      deviceId: identity.deviceId,
      packageRef,
      keyPackage,
      expiresAt,
    }), "utf8"),
    createPrivateKey({ key: identity.privateKeyJwk, format: "jwk" }),
  ).toString("base64url");
  return {
    packageRef,
    keyPackage,
    proof,
    expiresAt,
    uploaded: false,
    privatePackage: privatePackageToJson(generated.privatePackage),
  };
}

/** Ensure the server has a replenishable pool without ever uploading private key material. */
export async function ensureE2eeKeyPackages(client, knownStatus) {
  const status = knownStatus || await verifiedE2eeStatus(client);
  if (status.mode === "off") return status;
  if (status.protocol !== E2EE_PROTOCOL || status.cipherSuite !== E2EE_CIPHER_SUITE) {
    throw new Error("Relay's E2EE protocol does not match this Companion; refusing a downgrade or suite change.");
  }
  const identity = identityOrThrow();
  await syncTransparency(client, identity);
  const packageStatus = await client.e2eeKeyPackageStatus();
  const needed = Math.max(0, packageStatus.target - packageStatus.available);
  if (!needed) return status;

  const cs = await cipherSuite();
  const now = Date.now() + 60 * 60 * 1000;
  let local = readKeyPackageState(identity);
  let candidates = Object.values(local.packages)
    .filter((item) => !item.uploaded && Date.parse(item.expiresAt) > now)
    .slice(0, needed);
  const generated = [];
  while (candidates.length + generated.length < needed) generated.push(await generatePackage(identity, cs));
  if (generated.length) {
    mutateKeyPackageState(identity, (state) => {
      for (const item of generated) state.packages[item.packageRef] = item;
    });
  }
  local = readKeyPackageState(identity);
  candidates = Object.values(local.packages)
    .filter((item) => !item.uploaded && Date.parse(item.expiresAt) > now)
    .slice(0, needed);
  const response = await client.e2eeUploadKeyPackages(candidates.map(({ keyPackage, packageRef, proof, expiresAt }) => ({
    keyPackage,
    packageRef,
    proof,
    expiresAt,
  })));
  const accepted = new Set(response.accepted || []);
  mutateKeyPackageState(identity, (state) => {
    for (const packageRef of accepted) {
      if (state.packages[packageRef]) state.packages[packageRef].uploaded = true;
    }
  });
  return status;
}

export function clientConfigFor(transparency, checkpoint = null) {
  return {
    ...defaultClientConfig,
    lifetimeConfig: {
      maximumTotalLifetime: BigInt(KEY_PACKAGE_LIFETIME_SECONDS + 120),
      validateLifetimeOnReceive: true,
    },
    authService: {
      async validateCredential(credential, signaturePublicKey) {
        if (credential?.credentialType !== "basic") return false;
        let parsed;
        try { parsed = parseMlsCredential(textDecoder.decode(credential.identity)); } catch { return false; }
        if (!parsed) return false;
        const device = checkpoint
          ? deviceAtTransparencyCheckpoint(transparency, parsed.relayUserId, parsed.deviceId, checkpoint.size)
          : activeDeviceFromTransparency(transparency, parsed.relayUserId, parsed.deviceId);
        if (!device || device.fingerprint !== parsed.fingerprint) return false;
        return Buffer.from(signaturePublicKey).equals(Buffer.from(device.signaturePublicKey, "base64url"));
      },
    },
  };
}

async function assertPreparedTargets(client, identity, transparency, preparation) {
  const recipientSeen = new Set();
  const senderSeen = new Set();
  for (const target of preparation.targets) {
    if (recipientSeen.has(target.device.deviceId) || senderSeen.has(target.device.deviceId)) {
      throw new Error("Relay returned a duplicate encrypted delivery device.");
    }
    if (target.deliveryKind === "recipient") recipientSeen.add(target.device.deviceId);
    else if (target.deliveryKind === "sender_copy") senderSeen.add(target.device.deviceId);
    else throw new Error("Relay returned an unknown encrypted delivery kind.");
    const known = activeDeviceFromTransparency(
      transparency,
      target.relayUserId,
      target.device.deviceId,
    );
    if (
      !known ||
      known.protocol !== E2EE_PROTOCOL ||
      known.cipherSuite !== E2EE_CIPHER_SUITE ||
      known.signaturePublicKey !== target.device.signaturePublicKey ||
      known.fingerprint !== target.device.fingerprint
    ) throw new Error("Relay's recipient KeyPackage does not match the verified device directory.");
  }
  const expected = Object.entries(transparency.users?.[preparation.recipient.relayUserId]?.devices || {})
    .filter(([, device]) => !device.revokedAt && (!device.expiresAt || Date.parse(device.expiresAt) > Date.now()))
    .map(([deviceId]) => deviceId)
    .sort();
  const actual = [...recipientSeen].sort();
  if (!expected.length || expected.length !== actual.length || expected.some((deviceId, index) => deviceId !== actual[index])) {
    throw new Error("Relay omitted or added a recipient device relative to the verified E2EE directory.");
  }

  const directory = await client.e2eeDirectory(identity.userId);
  const activeSenderDevices = Object.fromEntries(
    Object.entries(transparency.users?.[identity.userId]?.devices || {})
      .filter(([, device]) => !device.revokedAt && (!device.expiresAt || Date.parse(device.expiresAt) > Date.now())),
  );
  const directoryById = new Map((directory.devices || []).map((device) => [device.deviceId, device]));
  const expectedDirectoryIds = Object.keys(activeSenderDevices).sort();
  const actualDirectoryIds = [...directoryById.keys()].sort();
  if (
    directory.relayUserId !== identity.userId ||
    expectedDirectoryIds.length !== actualDirectoryIds.length ||
    expectedDirectoryIds.some((deviceId, index) => deviceId !== actualDirectoryIds[index])
  ) throw new Error("Relay's sender device directory does not match key transparency.");
  for (const [deviceId, device] of directoryById) {
    const known = activeSenderDevices[deviceId];
    if (
      !known ||
      known.signaturePublicKey !== device.signaturePublicKey ||
      known.fingerprint !== device.fingerprint
    ) throw new Error("Relay's sender device identity does not match key transparency.");
  }

  const edges = [];
  for (const proof of directory.crossSignatures || []) {
    const signer = directoryById.get(proof.signerDeviceId);
    const subject = directoryById.get(proof.subjectDeviceId);
    if (!signer || !subject) continue;
    if (
      subject.fingerprint !== proof.subjectFingerprint ||
      !verifyDeviceCrossSignature({ relayUserId: identity.userId, signer, proof })
    ) throw new Error("Relay returned an invalid sender device approval proof.");
    edges.push([signer.deviceId, subject.deviceId]);
  }
  const trusted = new Set([identity.deviceId]);
  let changed = true;
  while (changed) {
    changed = false;
    for (const [left, right] of edges) {
      if (trusted.has(left) && !trusted.has(right)) { trusted.add(right); changed = true; }
      if (trusted.has(right) && !trusted.has(left)) { trusted.add(left); changed = true; }
    }
  }
  const expectedSenderCopies = [...trusted].sort();
  const actualSenderCopies = [...senderSeen].sort();
  if (
    expectedSenderCopies.length !== actualSenderCopies.length ||
    expectedSenderCopies.some((deviceId, index) => deviceId !== actualSenderCopies[index])
  ) throw new Error("Relay omitted or added a sender-copy device relative to the cross-signed trust graph.");
}

function attachmentAad(context, messageId, attachmentId) {
  return Buffer.from(JSON.stringify([context, messageId, attachmentId]), "utf8");
}

function sha256Hex(value) {
  return createHash("sha256").update(value).digest("hex");
}

function sha256Base64Url(value) {
  return createHash("sha256").update(value).digest("base64url");
}

function encryptAesGcm(key, iv, plaintext, aad) {
  const cipher = createCipheriv("aes-256-gcm", key, iv);
  cipher.setAAD(aad);
  return Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
}

function decryptAesGcm(key, iv, ciphertextAndTag, aad) {
  if (ciphertextAndTag.length < 17) throw new Error("Encrypted attachment envelope is too short.");
  const ciphertext = ciphertextAndTag.subarray(0, -16);
  const tag = ciphertextAndTag.subarray(-16);
  const decipher = createDecipheriv("aes-256-gcm", key, iv);
  decipher.setAAD(aad);
  decipher.setAuthTag(tag);
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
}

export function attachmentInputs(payload) {
  const items = Array.isArray(payload.attachments) ? payload.attachments : [];
  if (items.length > 20) throw new Error("Encrypted messages support at most 20 attachments.");
  let totalCiphertext = 0;
  return items.map((attachment, index) => {
    const encoded = String(attachment?.contentBase64 || "");
    if (!encoded) {
      throw new Error("Encrypted attachments must include their bytes on the sending device; nothing was sent.");
    }
    let body;
    try { body = Buffer.from(encoded, "base64"); } catch { body = null; }
    if (!body?.length || body.toString("base64").replace(/=+$/, "") !== encoded.replace(/=+$/, "")) {
      throw new Error("An encrypted attachment did not contain valid base64 file bytes.");
    }
    totalCiphertext += body.length + 16;
    if (totalCiphertext > E2EE_ATTACHMENT_TOTAL_CIPHERTEXT_BYTES) {
      throw new Error("Encrypted attachments are limited to 12 MiB total in the first release; nothing was sent.");
    }
    const name = String(attachment.name || `attachment-${index + 1}`).trim();
    const contentType = String(attachment.contentType || "application/octet-stream").trim();
    if (!name || name.length > 255 || !contentType || contentType.length > 255) {
      throw new Error("An encrypted attachment has an invalid filename or content type.");
    }
    const plaintextSha256 = sha256Hex(body);
    if (attachment.bytes !== undefined && Number(attachment.bytes) !== body.length) {
      throw new Error(`Attachment ${name} did not match its declared size.`);
    }
    if (attachment.sha256 && String(attachment.sha256).toLowerCase() !== plaintextSha256) {
      throw new Error(`Attachment ${name} did not match its declared SHA-256 hash.`);
    }
    return { name, contentType, body, plaintextSha256 };
  });
}

export function attachmentRequestBindings(items) {
  return items.map((item) => ({
    name: item.name,
    contentType: item.contentType,
    bytes: item.body.length,
    sha256: item.plaintextSha256,
  }));
}

export function encryptAttachments(messageId, inputs) {
  return inputs.map((input, index) => {
    const attachmentId = `eatt_${createHash("sha256")
      .update("relay-e2ee-attachment-id-v1")
      .update(messageId)
      .update(String(index))
      .update(input.plaintextSha256)
      .digest("base64url")}`;
    const key = randomBytes(32);
    const fileIv = randomBytes(12);
    const fileCiphertext = encryptAesGcm(
      key,
      fileIv,
      input.body,
      attachmentAad(ATTACHMENT_FILE_CONTEXT, messageId, attachmentId),
    );
    const ciphertextSha256 = sha256Base64Url(fileCiphertext);
    const metadata = {
      version: 1,
      attachmentId,
      name: input.name,
      contentType: input.contentType,
      plaintextSize: input.body.length,
      plaintextSha256: input.plaintextSha256,
      fileIv: fileIv.toString("base64url"),
      ciphertextSize: fileCiphertext.length,
      ciphertextSha256,
    };
    const metadataIv = randomBytes(12);
    const metadataCiphertext = encryptAesGcm(
      key,
      metadataIv,
      Buffer.from(JSON.stringify(metadata), "utf8"),
      attachmentAad(ATTACHMENT_METADATA_CONTEXT, messageId, attachmentId),
    );
    return {
      key: { attachmentId, key: key.toString("base64url") },
      wire: {
        attachmentId,
        encryptedMetadata: Buffer.concat([metadataIv, metadataCiphertext]).toString("base64url"),
        ciphertext: fileCiphertext.toString("base64url"),
        ciphertextSize: fileCiphertext.length,
        ciphertextSha256,
      },
    };
  });
}

export function decryptAttachmentMetadata(messageId, attachmentKeys, wireAttachments) {
  const keys = new Map();
  for (const item of attachmentKeys || []) {
    if (!item?.attachmentId || keys.has(item.attachmentId)) {
      throw new Error("Encrypted Relay contains invalid attachment key references.");
    }
    const key = Buffer.from(String(item.key || ""), "base64url");
    if (key.length !== 32 || key.toString("base64url") !== item.key) {
      throw new Error("Encrypted Relay contains an invalid attachment key.");
    }
    keys.set(item.attachmentId, key);
  }
  const wires = Array.isArray(wireAttachments) ? wireAttachments : [];
  if (wires.length !== keys.size) throw new Error("Encrypted Relay attachment routing does not match its MLS plaintext.");
  const seen = new Set();
  return wires.map((wire) => {
    if (!wire?.attachmentId || seen.has(wire.attachmentId) || !keys.has(wire.attachmentId)) {
      throw new Error("Encrypted Relay attachment routing does not match its MLS plaintext.");
    }
    seen.add(wire.attachmentId);
    try {
      const envelope = Buffer.from(wire.encryptedMetadata, "base64url");
      const metadataIv = envelope.subarray(0, 12);
      const metadata = JSON.parse(decryptAesGcm(
        keys.get(wire.attachmentId),
        metadataIv,
        envelope.subarray(12),
        attachmentAad(ATTACHMENT_METADATA_CONTEXT, messageId, wire.attachmentId),
      ).toString("utf8"));
      if (
        metadata.version !== 1 ||
        metadata.attachmentId !== wire.attachmentId ||
        typeof metadata.name !== "string" || !metadata.name || metadata.name.length > 255 ||
        typeof metadata.contentType !== "string" || !metadata.contentType || metadata.contentType.length > 255 ||
        !Number.isSafeInteger(metadata.plaintextSize) || metadata.plaintextSize < 0 ||
        !/^[a-f0-9]{64}$/.test(metadata.plaintextSha256) ||
        !/^[A-Za-z0-9_-]{16}$/.test(metadata.fileIv) ||
        metadata.ciphertextSize !== wire.ciphertextSize ||
        metadata.ciphertextSha256 !== wire.ciphertextSha256
      ) throw new Error("metadata mismatch");
      return {
        id: wire.attachmentId,
        name: metadata.name,
        contentType: metadata.contentType,
        bytes: metadata.plaintextSize,
        sha256: metadata.plaintextSha256,
        key: keys.get(wire.attachmentId).toString("base64url"),
        fileIv: metadata.fileIv,
        ciphertextSize: wire.ciphertextSize,
        ciphertextSha256: wire.ciphertextSha256,
        downloadUrl: wire.downloadUrl,
      };
    } catch (error) {
      if (/Encrypted Relay/.test(String(error?.message || ""))) throw error;
      throw new Error("Encrypted Relay attachment metadata could not be authenticated.");
    }
  });
}

function safeAttachmentFilename(name, index, attachmentId) {
  const clean = path.basename(String(name || "attachment")).replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").trim() || "attachment";
  return `${String(index + 1).padStart(2, "0")}-${attachmentId.slice(-10)}-${clean}`;
}

export async function materializeE2eeAttachments(messageId, metadata) {
  if (!metadata.length) return [];
  const root = path.join(path.dirname(identityPath()), "attachments", messageId.replace(/[^A-Za-z0-9_-]/g, "_"));
  fs.mkdirSync(root, { recursive: true, mode: 0o700 });
  const materialized = [];
  for (const [index, attachment] of metadata.entries()) {
    const filePath = path.join(root, safeAttachmentFilename(attachment.name, index, attachment.id));
    if (fs.existsSync(filePath)) {
      const existing = fs.readFileSync(filePath);
      if (existing.length === attachment.bytes && sha256Hex(existing) === attachment.sha256) {
        materialized.push({ ...attachment, localPath: filePath });
        continue;
      }
    }
    const response = await fetch(attachment.downloadUrl, { signal: AbortSignal.timeout(30_000) });
    if (!response.ok) throw new Error(`Encrypted attachment download failed with HTTP ${response.status}.`);
    const ciphertext = Buffer.from(await response.arrayBuffer());
    if (
      ciphertext.length !== attachment.ciphertextSize ||
      ciphertext.length > E2EE_ATTACHMENT_TOTAL_CIPHERTEXT_BYTES ||
      sha256Base64Url(ciphertext) !== attachment.ciphertextSha256
    ) throw new Error("Encrypted attachment bytes failed their ciphertext integrity check.");
    let plaintext;
    try {
      plaintext = decryptAesGcm(
        Buffer.from(attachment.key, "base64url"),
        Buffer.from(attachment.fileIv, "base64url"),
        ciphertext,
        attachmentAad(ATTACHMENT_FILE_CONTEXT, messageId, attachment.id),
      );
    } catch {
      throw new Error("Encrypted attachment bytes could not be authenticated.");
    }
    if (plaintext.length !== attachment.bytes || sha256Hex(plaintext) !== attachment.sha256) {
      throw new Error("Encrypted attachment plaintext failed its authenticated size or hash.");
    }
    const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
    fs.writeFileSync(tmp, plaintext, { mode: 0o600 });
    fs.renameSync(tmp, filePath);
    materialized.push({ ...attachment, localPath: filePath });
  }
  return materialized.map(({ key: _key, fileIv: _fileIv, ciphertextSize: _size, ciphertextSha256: _hash, downloadUrl: _url, ...safe }) => safe);
}

/** Preserve an author's plaintext files in the same encrypted-state-adjacent cache used after downloads. */
export function cacheE2eeAttachmentInputs(messageId, inputs, metadata) {
  if (!inputs.length) return;
  const root = path.join(path.dirname(identityPath()), "attachments", messageId.replace(/[^A-Za-z0-9_-]/g, "_"));
  fs.mkdirSync(root, { recursive: true, mode: 0o700 });
  for (const [index, attachment] of metadata.entries()) {
    const input = inputs[index];
    if (!input || input.body.length !== attachment.bytes || input.plaintextSha256 !== attachment.sha256) {
      throw new Error("Encrypted attachment cache input did not match its authenticated metadata.");
    }
    const filePath = path.join(root, safeAttachmentFilename(attachment.name, index, attachment.id));
    const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
    fs.writeFileSync(tmp, input.body, { mode: 0o600 });
    fs.renameSync(tmp, filePath);
  }
}

export function importE2eeAttachmentPlaintexts(messageId, metadata, contents) {
  const byId = new Map((contents || []).map((item) => [item.attachmentId, item]));
  const root = path.join(path.dirname(identityPath()), "attachments", messageId.replace(/[^A-Za-z0-9_-]/g, "_"));
  fs.mkdirSync(root, { recursive: true, mode: 0o700 });
  for (const [index, attachment] of (metadata || []).entries()) {
    const content = byId.get(attachment.id);
    if (!content || typeof content.contentBase64 !== "string") {
      throw new Error("Encrypted device history omitted an attachment body.");
    }
    const bytes = Buffer.from(content.contentBase64, "base64");
    if (bytes.length !== attachment.bytes || sha256Hex(bytes) !== attachment.sha256) {
      throw new Error("Encrypted device history attachment failed its size or hash check.");
    }
    const filePath = path.join(root, safeAttachmentFilename(attachment.name, index, attachment.id));
    const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
    fs.writeFileSync(tmp, bytes, { mode: 0o600 });
    fs.renameSync(tmp, filePath);
  }
  if (byId.size !== (metadata || []).length) {
    throw new Error("Encrypted device history contains unexpected attachment bodies.");
  }
}

export async function encryptE2eeMessage(client, payload, knownStatus) {
  const eventInput = payload.e2eeEvent || null;
  const kind = eventInput ? "message" : (payload.kind || "message");
  if (!["message", "task"].includes(kind)) throw new Error("Encrypted Relay supports messages and direct Tasks only.");
  if ((payload.files || []).length) throw new Error("Encrypted file paths must be prepared locally before sending.");
  if (payload.recipient?.groupId) throw new Error("Encrypted group messages are not available yet; nothing was sent.");
  const identity = identityOrThrow();
  const eventId = String(payload.messageId || `erelay_${createHash("sha256")
    .update("relay-e2ee-message-id-v1")
    .update(identity.deviceId)
    .update(String(payload.idempotencyKey || ""))
    .digest("base64url")}`);
  let recipient = payload.recipient || {};
  let conversation = null;
  let logicalMessageId = eventId;
  if (eventInput?.targetRelayId) {
    const target = await encryptedRecord(client, String(eventInput.targetRelayId));
    logicalMessageId = target.plaintext.messageId;
    conversation = target.plaintext.conversation;
    recipient = { relayUserId: otherParticipant(identity, target.wire) };
    if (
      (eventInput.type === "message.edited" || eventInput.type === "message.deleted") &&
      target.plaintext.senderUserId !== identity.userId
    ) throw new Error("Only the sender can edit or delete an encrypted message.");
  } else if (payload.inReplyToRelayId) {
    const target = await encryptedRecord(client, String(payload.inReplyToRelayId));
    recipient = { relayUserId: otherParticipant(identity, target.wire) };
    conversation = {
      ...target.plaintext.conversation,
      replyToMessageId: target.plaintext.messageId,
    };
  }
  const outboxId = `direct:${eventId}`;
  const inputAttachments = attachmentInputs(payload);
  const requestHash = e2eeRequestHash("relay-e2ee-direct-send-request-v1", {
    eventId,
    recipient,
    title: String(payload.title || "").trim() || null,
    forHuman: String(payload.forHuman || ""),
    forAgent: String(payload.forAgent || ""),
    kind,
    type: payload.type || null,
    targetSurfaces: encryptedTargetSurfaces(payload.targetSurfaces),
    source: encryptedSource(payload.source),
    attachments: attachmentRequestBindings(inputAttachments),
    historyImport: payload.historyImport === true,
    historyImportEdited: payload.historyImportEdited === true,
    historyImportDeleted: payload.historyImportDeleted === true,
    historyImportTaskState: payload.historyImportTaskState || null,
    conversation,
    eventInput,
    idempotencyKey: String(payload.idempotencyKey || ""),
  });
  const existingOutbox = readPendingE2eeOutbox(identity, outboxId);
  if (existingOutbox) {
    if (existingOutbox.requestHash !== requestHash) {
      throw new Error("That encrypted idempotency key is already bound to different content.");
    }
    if (existingOutbox.status === "completed") return existingOutbox.response;
    const response = await client.e2eeSendMessage(existingOutbox.wire);
    const completed = completeE2eeOutbox(identity, outboxId, requestHash, response);
    if (existingOutbox.currentSenderPackageRef) {
      mutateKeyPackageState(identity, (state) => { delete state.packages[existingOutbox.currentSenderPackageRef]; });
    }
    return completed;
  }
  await ensureE2eeKeyPackages(client, knownStatus);
  const preparation = await client.e2eePrepareSend({
    recipient,
    idempotencyKey: `${payload.idempotencyKey}:prepare`,
  });
  const transparency = await syncTransparency(client, identity);
  assertKnownCheckpoint(transparency, preparation.checkpoint);
  await assertE2eeRecipientDevicesTrusted(client, preparation.recipient.relayUserId, { identity });
  await assertPreparedTargets(client, identity, transparency, preparation);

  const cs = await cipherSuite();
  const senderPackage = await generatePackage(identity, cs);
  const senderDecoded = decodeExact(decodeMlsMessage, senderPackage.keyPackage, "sender MLS KeyPackage");
  if (senderDecoded.wireformat !== "mls_key_package") throw new Error("Expected an MLS KeyPackage for the sender.");
  let state = await createGroup(
    textEncoder.encode(`relay:${eventId}`),
    senderDecoded.keyPackage,
    privatePackageFromJson(senderPackage.privatePackage),
    [],
    cs,
    clientConfigFor(transparency),
  );
  const currentSenderTarget = preparation.targets.find((target) =>
    target.deliveryKind === "sender_copy" && target.device.deviceId === identity.deviceId);
  if (!currentSenderTarget) throw new Error("Relay did not prepare a synchronized copy for this sending device.");
  const proposals = preparation.targets
    .filter((target) => target.device.deviceId !== identity.deviceId)
    .map((target) => {
    const decoded = decodeExact(decodeMlsMessage, target.keyPackage, "recipient MLS KeyPackage");
    if (decoded.wireformat !== "mls_key_package") throw new Error("Expected a recipient MLS KeyPackage.");
    return { proposalType: "add", add: { keyPackage: decoded.keyPackage } };
  });
  const committed = await createCommit(
    { state, cipherSuite: cs },
    { extraProposals: proposals, ratchetTreeExtension: true },
  );
  if (!committed.welcome) throw new Error("MLS did not create a Welcome for the recipient devices.");
  state = committed.newState;
  committed.consumed.forEach(zeroOutUint8Array);

  const authoredAt = new Date().toISOString();
  const attachmentEnvelopes = encryptAttachments(eventId, inputAttachments);
  const eventBase = {
    version: 2,
    eventId,
    messageId: logicalMessageId,
    senderUserId: identity.userId,
    senderDeviceId: identity.deviceId,
    recipientUserId: preparation.recipient.relayUserId,
    recipientDeviceIds: preparation.targets
      .filter((target) => target.deliveryKind === "recipient")
      .map((target) => target.device.deviceId)
      .sort(),
    senderDeviceIds: preparation.targets
      .filter((target) => target.deliveryKind === "sender_copy")
      .map((target) => target.device.deviceId)
      .sort(),
    authoredAt,
    conversation: conversation || {
      conversationId: directConversationId(identity.userId, preparation.recipient.relayUserId),
      threadId: eventId,
    },
    transparencyCheckpoint: { ...transparency.checkpoint },
    ...(payload.historyImport ? { historyImport: { version: 1 } } : {}),
  };
  let plaintext;
  switch (eventInput?.type || "message.created") {
    case "message.edited":
      plaintext = {
        ...eventBase,
        eventType: "message.edited",
        revision: Math.max(2, Number(eventInput.revision || 2)),
        ...(payload.forHuman !== undefined ? { forHuman: String(payload.forHuman || "") } : {}),
        ...(payload.forAgent !== undefined ? { forAgent: String(payload.forAgent || "") } : {}),
        ...(payload.title !== undefined ? { title: String(payload.title || "").trim() || null } : {}),
        editedAt: authoredAt,
      };
      break;
    case "message.deleted":
      plaintext = {
        ...eventBase,
        eventType: "message.deleted",
        revision: Math.max(2, Number(eventInput.revision || 2)),
        deletedAt: authoredAt,
      };
      break;
    case "reaction.changed":
      plaintext = {
        ...eventBase,
        eventType: "reaction.changed",
        reaction: {
          reactionId: `ereact_${createHash("sha256")
            .update("relay-e2ee-reaction-v1")
            .update(logicalMessageId)
            .update(identity.userId)
            .update(String(eventInput.emoji || ""))
            .digest("base64url")}`,
          emoji: String(eventInput.emoji || ""),
          action: eventInput.action,
          reactedAt: authoredAt,
        },
      };
      break;
    case "receipt.changed":
      plaintext = {
        ...eventBase,
        eventType: "receipt.changed",
        receipt: { state: eventInput.state, occurredAt: authoredAt },
      };
      break;
    case "task.changed":
      plaintext = {
        ...eventBase,
        eventType: "task.changed",
        task: {
          taskId: String(eventInput.taskId || logicalMessageId),
          state: eventInput.state,
          occurredAt: authoredAt,
          ...(eventInput.resultMessageId ? { resultMessageId: String(eventInput.resultMessageId) } : {}),
          ...(eventInput.taskRunOwner ? { taskRunOwner: eventInput.taskRunOwner } : {}),
        },
      };
      break;
    default:
      plaintext = {
        ...eventBase,
        eventType: "message.created",
        revision: 1,
        kind,
        ...(payload.type ? { type: payload.type } : {}),
        ...(String(payload.title || "").trim() ? { title: String(payload.title).trim() } : {}),
        forHuman: String(payload.forHuman || ""),
        forAgent: String(payload.forAgent || ""),
        targetSurfaces: encryptedTargetSurfaces(payload.targetSurfaces),
        ...(encryptedSource(payload.source) ? { source: encryptedSource(payload.source) } : {}),
        ...(kind === "task" ? {
          task: {
            taskId: eventId,
            state: payload.historyImport && payload.historyImportTaskState
              ? payload.historyImportTaskState
              : "requested",
            occurredAt: authoredAt,
          },
        } : {}),
        ...(payload.historyImport && payload.historyImportEdited ? { editedAt: authoredAt } : {}),
        ...(payload.historyImport && payload.historyImportDeleted ? { deletedAt: authoredAt } : {}),
        attachments: attachmentEnvelopes.map((attachment) => attachment.key),
      };
  }
  if (!plaintext.eventId || (plaintext.eventType === "message.created" && !plaintext.forHuman)) {
    throw new Error("Encrypted text requires an event id and human-facing body.");
  }
  const encrypted = await createApplicationMessage(state, textEncoder.encode(JSON.stringify(plaintext)), cs);
  encrypted.consumed.forEach(zeroOutUint8Array);
  const welcome = encodeMlsMessage({ version: E2EE_PROTOCOL, wireformat: "mls_welcome", welcome: committed.welcome });
  const ratchetTree = encodeRatchetTree(state.ratchetTree);
  const ciphertext = encodeMlsMessage({
    version: E2EE_PROTOCOL,
    wireformat: "mls_private_message",
    privateMessage: encrypted.privateMessage,
  });
  const wire = {
    preparationId: preparation.preparationId,
    messageId: eventId,
    welcome: b64(welcome),
    ratchetTree: b64(ratchetTree),
    ciphertext: b64(ciphertext),
    attachments: attachmentEnvelopes.map((attachment) => attachment.wire),
    senderCheckpoint: { ...transparency.checkpoint },
    idempotencyKey: payload.idempotencyKey,
    ...(payload.historyImport ? { historyImport: true } : {}),
  };
  // The MLS creator is already a group member, so it cannot consume its own
  // Welcome. Persist its authenticated plaintext in the encrypted local cache;
  // the server still exposes a sender-copy delivery marker, and every sibling
  // sender device receives a normal Welcome.
  cachePlaintext(identity, eventId, plaintext);
  const durable = writePendingE2eeOutbox(identity, outboxId, {
    requestHash,
    wire,
    currentSenderPackageRef: currentSenderTarget.packageRef,
  });
  if (durable.status === "completed") return durable.response;
  const response = await client.e2eeSendMessage(durable.wire);
  const completed = completeE2eeOutbox(identity, outboxId, requestHash, response);
  mutateKeyPackageState(identity, (state) => { delete state.packages[currentSenderTarget.packageRef]; });
  return completed;
}

function validateDecryptedPlaintext(plaintext, wire, identity, transparency) {
  const authoredAt = typeof plaintext?.authoredAt === "string" ? new Date(plaintext.authoredAt) : null;
  const eventTypes = new Set([
    "message.created",
    "message.edited",
    "message.deleted",
    "reaction.changed",
    "receipt.changed",
    "task.changed",
  ]);
  if (
    plaintext?.version !== 2 ||
    plaintext.eventId !== wire.relayId ||
    typeof plaintext.messageId !== "string" ||
    !eventTypes.has(plaintext.eventType) ||
    plaintext.senderUserId !== wire.sender.relayUserId ||
    plaintext.senderDeviceId !== wire.sender.deviceId ||
    plaintext.recipientUserId !== wire.recipient.relayUserId ||
    !Array.isArray(plaintext.recipientDeviceIds) ||
    !Array.isArray(plaintext.senderDeviceIds) ||
    wire.delivery?.deviceId !== identity.deviceId ||
    wire.delivery?.ownerUserId !== identity.userId ||
    (
      wire.delivery?.kind === "recipient"
        ? plaintext.recipientUserId !== identity.userId || !plaintext.recipientDeviceIds.includes(identity.deviceId)
        : wire.delivery?.kind === "sender_copy"
          ? plaintext.senderUserId !== identity.userId || !plaintext.senderDeviceIds.includes(identity.deviceId)
          : true
    ) ||
    !authoredAt ||
    !Number.isFinite(authoredAt.getTime()) ||
    authoredAt.toISOString() !== plaintext.authoredAt ||
    typeof plaintext.conversation?.conversationId !== "string" ||
    typeof plaintext.conversation?.threadId !== "string" ||
    (plaintext.historyImport !== undefined && plaintext.historyImport?.version !== 1) ||
    plaintext.transparencyCheckpoint?.size !== wire.senderCheckpoint.size ||
    plaintext.transparencyCheckpoint?.headHash !== wire.senderCheckpoint.headHash ||
    (plaintext.eventType === "message.created" && (
      plaintext.messageId !== plaintext.eventId ||
      plaintext.revision !== 1 ||
      !["message", "task"].includes(plaintext.kind || "message") ||
      (plaintext.type !== undefined && !["question", "completion"].includes(plaintext.type)) ||
      typeof plaintext.forHuman !== "string" ||
      !plaintext.forHuman ||
      typeof plaintext.forAgent !== "string" ||
      !Array.isArray(plaintext.targetSurfaces || []) ||
      (plaintext.kind === "task" && (
        plaintext.task?.taskId !== plaintext.eventId ||
        (!plaintext.historyImport && plaintext.task?.state !== "requested") ||
        !["requested", "accepted", "started", "completed", "failed", "declined", "cancelled"].includes(plaintext.task?.state) ||
        plaintext.task?.occurredAt !== plaintext.authoredAt
      )) ||
      (plaintext.kind !== "task" && plaintext.task !== undefined) ||
      ((plaintext.editedAt !== undefined || plaintext.deletedAt !== undefined) && !plaintext.historyImport) ||
      (plaintext.editedAt !== undefined && plaintext.editedAt !== plaintext.authoredAt) ||
      (plaintext.deletedAt !== undefined && plaintext.deletedAt !== plaintext.authoredAt) ||
      !Array.isArray(plaintext.attachments || [])
    )) ||
    (plaintext.eventType === "message.edited" && (
      !Number.isInteger(plaintext.revision) || plaintext.revision < 2 ||
      (plaintext.forHuman === undefined && plaintext.forAgent === undefined && plaintext.title === undefined) ||
      (plaintext.forHuman !== undefined && (typeof plaintext.forHuman !== "string" || !plaintext.forHuman)) ||
      (plaintext.forAgent !== undefined && typeof plaintext.forAgent !== "string") ||
      plaintext.editedAt !== plaintext.authoredAt
    )) ||
    (plaintext.eventType === "message.deleted" && (
      !Number.isInteger(plaintext.revision) || plaintext.revision < 2 ||
      plaintext.deletedAt !== plaintext.authoredAt
    )) ||
    (plaintext.eventType === "reaction.changed" && (
      typeof plaintext.reaction?.reactionId !== "string" ||
      typeof plaintext.reaction?.emoji !== "string" || !plaintext.reaction.emoji ||
      !["add", "remove"].includes(plaintext.reaction?.action) ||
      plaintext.reaction?.reactedAt !== plaintext.authoredAt
    )) ||
    (plaintext.eventType === "receipt.changed" && (
      !["delivered", "read", "acknowledged"].includes(plaintext.receipt?.state) ||
      plaintext.receipt?.occurredAt !== plaintext.authoredAt
    )) ||
    (plaintext.eventType === "task.changed" && (
      typeof plaintext.task?.taskId !== "string" ||
      !["requested", "accepted", "started", "completed", "failed", "declined", "cancelled"].includes(plaintext.task?.state) ||
      plaintext.task?.occurredAt !== plaintext.authoredAt ||
      (plaintext.task?.taskRunOwner !== undefined && (
        !["relay_work", "external_mcp"].includes(plaintext.task.taskRunOwner?.kind) ||
        (plaintext.task.taskRunOwner.provider !== undefined && !["codex", "claude"].includes(plaintext.task.taskRunOwner.provider)) ||
        (plaintext.task.taskRunOwner.nativeSessionId !== undefined && typeof plaintext.task.taskRunOwner.nativeSessionId !== "string")
      ))
    ))
  ) throw new Error("Encrypted Relay metadata did not authenticate against its MLS plaintext.");
  assertKnownCheckpoint(transparency, plaintext.transparencyCheckpoint);
  const attachmentKeys = ["message.created", "message.edited"].includes(plaintext.eventType)
    ? plaintext.attachments || []
    : [];
  return {
    ...plaintext,
    attachments: attachmentKeys,
    attachmentMetadata: decryptAttachmentMetadata(
      plaintext.eventId,
      attachmentKeys,
      wire.attachments || [],
    ),
  };
}

export async function decryptE2eeWireMessage(client, wire) {
  const identity = identityOrThrow();
  const transparency = await syncTransparency(client, identity);
  assertKnownCheckpoint(transparency, wire.senderCheckpoint);
  await assertE2eeSenderDeviceTrusted(client, wire.sender.relayUserId, wire.sender.deviceId, { identity });
  const cached = readCachedPlaintext(identity, wire.relayId);
  if (cached) return validateDecryptedPlaintext(cached, wire, identity, transparency);

  const packageState = readKeyPackageState(identity);
  const localPackage = packageState.packages[wire.keyPackageRef];
  if (!localPackage) {
    throw new Error("This device no longer has the one-time MLS KeyPackage needed to open this Relay.");
  }
  const cs = await cipherSuite();
  const keyPackageMessage = decodeExact(decodeMlsMessage, localPackage.keyPackage, "local MLS KeyPackage");
  const welcomeMessage = decodeExact(decodeMlsMessage, wire.welcome, "MLS Welcome");
  const privateMessage = decodeExact(decodeMlsMessage, wire.ciphertext, "MLS ciphertext");
  const ratchetTree = decodeExact(decodeRatchetTree, wire.ratchetTree, "MLS ratchet tree");
  if (
    keyPackageMessage.wireformat !== "mls_key_package" ||
    welcomeMessage.wireformat !== "mls_welcome" ||
    privateMessage.wireformat !== "mls_private_message"
  ) throw new Error("Encrypted Relay contains the wrong MLS wire formats.");
  const group = await joinGroup(
    welcomeMessage.welcome,
    keyPackageMessage.keyPackage,
    privatePackageFromJson(localPackage.privatePackage),
    emptyPskIndex,
    cs,
    ratchetTree,
    undefined,
    clientConfigFor(transparency, wire.senderCheckpoint),
  );
  const opened = await processPrivateMessage(group, privateMessage.privateMessage, emptyPskIndex, cs);
  if (opened.kind !== "applicationMessage") throw new Error("Encrypted Relay did not contain an MLS application message.");
  let plaintext;
  try { plaintext = JSON.parse(textDecoder.decode(opened.message)); } catch {
    throw new Error("Encrypted Relay plaintext is not valid UTF-8 JSON.");
  } finally {
    opened.consumed.forEach(zeroOutUint8Array);
  }
  plaintext = validateDecryptedPlaintext(plaintext, wire, identity, transparency);
  cachePlaintext(identity, wire.relayId, plaintext);
  mutateKeyPackageState(identity, (state) => { delete state.packages[wire.keyPackageRef]; });
  return plaintext;
}

function taskReceiptFields(plaintext) {
  if ((plaintext.kind || "message") !== "task" || !plaintext.task) return {};
  const state = plaintext.task.state;
  return {
    taskId: plaintext.task.taskId,
    taskState: plaintext.taskState || state,
    ...(plaintext.taskAcceptedAt || state === "accepted"
      ? { taskAcceptedAt: plaintext.taskAcceptedAt || plaintext.task.occurredAt }
      : {}),
    ...(plaintext.taskStartedAt || ["started", "completed"].includes(state)
      ? { taskStartedAt: plaintext.taskStartedAt || plaintext.task.occurredAt }
      : {}),
    ...(plaintext.taskRunOwner || plaintext.task.taskRunOwner
      ? { taskRunOwner: plaintext.taskRunOwner || plaintext.task.taskRunOwner }
      : {}),
    ...(plaintext.taskCompletedAt || state === "completed"
      ? { taskCompletedAt: plaintext.taskCompletedAt || plaintext.task.occurredAt }
      : {}),
  };
}

function encryptedItemFields(plaintext) {
  return {
    kind: plaintext.kind || "message",
    ...(plaintext.type ? { type: plaintext.type } : {}),
    source: plaintext.source || { host: "relay-companion" },
    targetSurfaces: plaintext.targetSurfaces || [],
    ...(plaintext.historyImport ? { historyImported: true } : {}),
    ...taskReceiptFields(plaintext),
  };
}

export function e2eeInboxItem(wire, plaintext) {
  const attachments = (plaintext.attachmentMetadata || []).map((attachment) => ({
    id: attachment.id,
    name: attachment.name,
    contentType: attachment.contentType,
    bytes: attachment.bytes,
    sha256: attachment.sha256,
    openUrl: attachment.downloadUrl,
  }));
  return {
    relayId: wire.relayId,
    state: plaintext.historyImport ? "acknowledged" : plaintext.localReceiptState || wire.state,
    createdAt: plaintext.createdAt || plaintext.authoredAt,
    updatedAt: plaintext.updatedAt || wire.updatedAt,
    ...encryptedItemFields(plaintext),
    ...(plaintext.title ? { title: plaintext.title, displayTitle: plaintext.title } : {}),
    sender: { relayUserId: wire.sender.relayUserId, name: wire.sender.name },
    preview: plaintext.deletedAt ? "Message deleted" : plaintext.forHuman,
    forHuman: plaintext.deletedAt ? "Message deleted" : plaintext.forHuman,
    forAgent: plaintext.forAgent || "",
    ...(plaintext.conversation?.replyToMessageId ? { inReplyToRelayId: plaintext.conversation.replyToMessageId } : {}),
    threadId: plaintext.conversation?.threadId || wire.relayId,
    ...(plaintext.editedAt ? { editedAt: plaintext.editedAt } : {}),
    ...(plaintext.deletedAt ? { deletedAt: plaintext.deletedAt } : {}),
    hasAttachments: attachments.length > 0,
    attachments,
    reactions: plaintext.reactions || { aggregates: [], events: [] },
    e2ee: { protocol: E2EE_PROTOCOL, cipherSuite: E2EE_CIPHER_SUITE, senderDeviceId: wire.sender.deviceId },
  };
}

export function e2eeSentItem(wire, plaintext) {
  const attachments = (plaintext.attachmentMetadata || []).map((attachment) => ({
    id: attachment.id,
    name: attachment.name,
    contentType: attachment.contentType,
    bytes: attachment.bytes,
    sha256: attachment.sha256,
    openUrl: attachment.downloadUrl,
  }));
  return {
    relayId: wire.relayId,
    state: plaintext.recipientReceiptState || "delivered",
    createdAt: plaintext.createdAt || plaintext.authoredAt,
    updatedAt: plaintext.updatedAt || wire.updatedAt,
    ...(plaintext.recipientReadAt ? { readAt: plaintext.recipientReadAt } : {}),
    ...encryptedItemFields(plaintext),
    ...(plaintext.title ? { title: plaintext.title, displayTitle: plaintext.title } : {}),
    recipient: { name: wire.recipient.name, onRelay: true },
    preview: plaintext.deletedAt ? "Message deleted" : plaintext.forHuman,
    forHuman: plaintext.deletedAt ? "Message deleted" : plaintext.forHuman,
    forAgent: plaintext.forAgent || "",
    ...(plaintext.conversation?.replyToMessageId ? { inReplyToRelayId: plaintext.conversation.replyToMessageId } : {}),
    threadId: plaintext.conversation?.threadId || wire.relayId,
    ...(plaintext.editedAt ? { editedAt: plaintext.editedAt } : {}),
    ...(plaintext.deletedAt ? { deletedAt: plaintext.deletedAt } : {}),
    delivery: {
      channel: "device",
      state: plaintext.recipientReceiptState || "delivered",
      sentAt: wire.createdAt,
      ...(plaintext.recipientReadAt ? { openedAt: plaintext.recipientReadAt } : {}),
    },
    hasAttachments: attachments.length > 0,
    attachments,
    reactions: plaintext.reactions || { aggregates: [], events: [] },
    e2ee: { protocol: E2EE_PROTOCOL, cipherSuite: E2EE_CIPHER_SUITE, senderDeviceId: wire.sender.deviceId },
  };
}

export function e2eeThreadItem(wire, plaintext) {
  const outbound = wire.delivery.kind === "sender_copy";
  return {
    ...(outbound ? e2eeSentItem(wire, plaintext) : e2eeInboxItem(wire, plaintext)),
    sender: { relayUserId: wire.sender.relayUserId, name: wire.sender.name },
    recipient: { relayUserId: wire.recipient.relayUserId, name: wire.recipient.name, onRelay: true },
    direction: outbound ? "outbound" : "inbound",
    readReceipts: outbound
      ? [{ name: wire.recipient.name, seen: Boolean(plaintext.recipientReadAt), ...(plaintext.recipientReadAt ? { readAt: plaintext.recipientReadAt } : {}) }]
      : [],
  };
}

export async function e2eePacket(wire, plaintext) {
  const attachments = await materializeE2eeAttachments(wire.relayId, plaintext.attachmentMetadata || []);
  return {
    packet: {
      schemaVersion: 3,
      id: wire.relayId,
      relayId: wire.relayId,
      createdAt: plaintext.createdAt || plaintext.authoredAt,
      ...encryptedItemFields(plaintext),
      ...(plaintext.title ? { title: plaintext.title, displayTitle: plaintext.title } : {}),
      forHuman: plaintext.deletedAt ? "Message deleted" : plaintext.forHuman,
      forAgent: plaintext.forAgent || "",
      sender: { relayUserId: wire.sender.relayUserId, name: wire.sender.name },
      recipient: { relayUserId: wire.recipient.relayUserId, name: "You" },
      ...(plaintext.conversation?.replyToMessageId ? { inReplyToRelayId: plaintext.conversation.replyToMessageId } : {}),
      threadId: plaintext.conversation?.threadId || wire.relayId,
      ...(plaintext.editedAt ? { editedAt: plaintext.editedAt } : {}),
      ...(plaintext.deletedAt ? { deletedAt: plaintext.deletedAt } : {}),
      reactions: plaintext.reactions || { aggregates: [], events: [] },
      attachments,
      e2ee: { protocol: E2EE_PROTOCOL, cipherSuite: E2EE_CIPHER_SUITE, senderDeviceId: wire.sender.deviceId },
    },
    attachmentUrls: {},
  };
}

export function newE2eeMessageId() {
  return `erelay_${randomBytes(18).toString("base64url")}`;
}